claude-dev-env 1.83.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/_shared/pr-loop/audit-contract.md +24 -12
- 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/reviews_disabled.py +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_reviews_disabled.py +14 -0
- 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 +0 -4
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +116 -6
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +11 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- 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_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -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_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +3 -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/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 +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/autoconverge/SKILL.md +38 -57
- 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 +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +70 -73
- 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/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
|
@@ -177,6 +177,39 @@ def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
|
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def test_scan_function_body_constants_skips_docstring_contrast_line() -> None:
|
|
181
|
+
source = (
|
|
182
|
+
"def demo(label):\n"
|
|
183
|
+
' """Flag a boolean assignment whose target and callee clash.\n'
|
|
184
|
+
"\n"
|
|
185
|
+
" ::\n"
|
|
186
|
+
"\n"
|
|
187
|
+
" FLAG: ready = make_stale(...)\n"
|
|
188
|
+
' """\n'
|
|
189
|
+
" return label\n"
|
|
190
|
+
)
|
|
191
|
+
advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
|
|
192
|
+
assert advisory_issues == [], (
|
|
193
|
+
f"Expected no function-local constant issue on an illustrative row, got: {advisory_issues}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_scan_function_body_constants_still_flags_real_function_body_constant() -> None:
|
|
198
|
+
source = (
|
|
199
|
+
"def demo(label):\n"
|
|
200
|
+
' """Flag a boolean assignment whose target and callee clash.\n'
|
|
201
|
+
"\n"
|
|
202
|
+
" FLAG: ready = make_stale(...)\n"
|
|
203
|
+
' """\n'
|
|
204
|
+
" SOME_CONST = 3\n"
|
|
205
|
+
" return SOME_CONST\n"
|
|
206
|
+
)
|
|
207
|
+
advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
|
|
208
|
+
assert any("SOME_CONST" in issue for issue in advisory_issues), (
|
|
209
|
+
f"Expected the real function-body constant SOME_CONST to stay flagged, got: {advisory_issues}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
180
213
|
def test_check_constants_outside_config_flags_annotated_assignment() -> None:
|
|
181
214
|
source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
|
|
182
215
|
issues = code_rules_enforcer.check_constants_outside_config(
|
|
@@ -18,14 +18,37 @@ from code_rules_enforcer import ( # noqa: E402
|
|
|
18
18
|
)
|
|
19
19
|
from code_rules_shared import ( # noqa: E402
|
|
20
20
|
changed_line_numbers,
|
|
21
|
+
docstring_line_numbers,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
code_rules_enforcer = SimpleNamespace(
|
|
24
25
|
changed_line_numbers=changed_line_numbers,
|
|
26
|
+
docstring_line_numbers=docstring_line_numbers,
|
|
25
27
|
prior_and_post_edit_content=prior_and_post_edit_content,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def test_docstring_line_numbers_spans_a_multi_line_function_docstring() -> None:
|
|
32
|
+
source = (
|
|
33
|
+
"def describe():\n"
|
|
34
|
+
' """First row.\n'
|
|
35
|
+
"\n"
|
|
36
|
+
" Third row.\n"
|
|
37
|
+
' """\n'
|
|
38
|
+
" return 1\n"
|
|
39
|
+
)
|
|
40
|
+
assert code_rules_enforcer.docstring_line_numbers(source) == {2, 3, 4, 5}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_docstring_line_numbers_excludes_real_code_lines() -> None:
|
|
44
|
+
source = "def compute():\n counter = 41\n return counter\n"
|
|
45
|
+
assert code_rules_enforcer.docstring_line_numbers(source) == set()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_docstring_line_numbers_returns_empty_set_on_syntax_error() -> None:
|
|
49
|
+
assert code_rules_enforcer.docstring_line_numbers("def broken(:\n") == set()
|
|
50
|
+
|
|
51
|
+
|
|
29
52
|
def test_readable_prior_yields_consistent_prior_and_reconstruction(tmp_path) -> None:
|
|
30
53
|
"""When the prior reads cleanly, the helper returns the same prior content it
|
|
31
54
|
reconstructed the post-edit view from, so the two never diverge across two
|
|
@@ -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"]
|
|
@@ -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:
|
|
@@ -24,7 +24,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
24
24
|
| `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
|
|
25
25
|
| `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
|
|
26
26
|
| `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
|
|
27
|
-
| `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
|
|
28
27
|
| `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
|
|
29
28
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
30
29
|
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
@@ -34,10 +33,8 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
34
33
|
| `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
|
|
35
34
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
36
35
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
37
|
-
| `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
|
|
38
36
|
| `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
|
|
39
37
|
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
40
|
-
| `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
|
|
41
38
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
42
39
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
43
40
|
| `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
|
|
@@ -218,6 +218,9 @@ DOCSTRING_RUNON_SENTENCE_WORD_LIMIT: int = 30
|
|
|
218
218
|
MAX_DOCSTRING_RUNON_SENTENCE_ISSUES: int = 5
|
|
219
219
|
ALL_DOCSTRING_RUNON_JOINER_MARKERS: tuple[str, ...] = ("—", " -- ", ";")
|
|
220
220
|
DOCSTRING_RUNON_SENTENCE_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"(?<=[.!?])\s+")
|
|
221
|
+
DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT: int = 6
|
|
222
|
+
DOCSTRING_NARRATIVE_LINE_JOIN_SEPARATOR: str = " "
|
|
223
|
+
MAX_DOCSTRING_PROSE_WALL_ISSUES: int = 5
|
|
221
224
|
|
|
222
225
|
ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
|
|
223
226
|
"no consumer reads",
|
|
@@ -22,8 +22,8 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
|
22
22
|
"pr_converge_skill_constants",
|
|
23
23
|
"pr_converge_scripts_constants",
|
|
24
24
|
"bugteam_scripts_constants",
|
|
25
|
-
"doc_gist_scripts_constants",
|
|
26
25
|
"implement_scripts_constants",
|
|
26
|
+
"log_audit_constants",
|
|
27
27
|
"dev_env_scripts_constants",
|
|
28
28
|
}
|
|
29
29
|
)
|
|
@@ -62,9 +62,4 @@ ALL_POST_HOSTED_HOOK_ENTRIES: tuple[PostHostedHookEntry, ...] = (
|
|
|
62
62
|
script_relative_path="workflow/auto_formatter.py",
|
|
63
63
|
is_blocking=False,
|
|
64
64
|
),
|
|
65
|
-
PostHostedHookEntry(
|
|
66
|
-
script_relative_path="workflow/doc_gist_auto_publish.py",
|
|
67
|
-
extra_argument_relative_paths=(PLUGIN_ROOT_PLACEHOLDER,),
|
|
68
|
-
is_blocking=False,
|
|
69
|
-
),
|
|
70
65
|
)
|
|
@@ -168,7 +168,7 @@ class PostDispatcherDecision:
|
|
|
168
168
|
all_block_reasons: All block reasons from blocking hooks, in run order.
|
|
169
169
|
all_non_block_stdout: Stdout from hooks that did not emit a block
|
|
170
170
|
decision, concatenated in run order. Preserved so informational
|
|
171
|
-
output (such as
|
|
171
|
+
output (such as an auto-formatter notice) reaches the harness
|
|
172
172
|
on both the allow and block paths.
|
|
173
173
|
"""
|
|
174
174
|
|
|
@@ -5,11 +5,11 @@ hook as its own subprocess (the production path), records each hook's block
|
|
|
5
5
|
decision, computes the expected aggregate, then runs the dispatcher on the same
|
|
6
6
|
payload and asserts an equal block-or-allow decision and the union of reasons.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Two focused tests pin the side-effecting behavior the dispatcher must not
|
|
9
9
|
change: the formatter formats only on a Write of a file git does not yet track
|
|
10
|
-
and never blocks; the type-checker still blocks on a real type error when
|
|
11
|
-
through the dispatcher
|
|
12
|
-
|
|
10
|
+
and never blocks; and the type-checker still blocks on a real type error when
|
|
11
|
+
run through the dispatcher. Two more tests pin that non-block stdout from a
|
|
12
|
+
side-effect hook survives on both the allow and block paths.
|
|
13
13
|
|
|
14
14
|
Crash and early-exit tests exercise the aggregator directly: an early hook
|
|
15
15
|
crash before mypy does not drop mypy's block, a non-blocking hook crash leaves
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import importlib.util
|
|
22
22
|
import json
|
|
23
|
+
import os
|
|
23
24
|
import subprocess
|
|
24
25
|
import sys
|
|
25
26
|
from pathlib import Path
|
|
@@ -33,9 +34,6 @@ _VALIDATION_DIR_STR = str(Path(__file__).resolve().parent)
|
|
|
33
34
|
if _VALIDATION_DIR_STR not in sys.path:
|
|
34
35
|
sys.path.insert(0, _VALIDATION_DIR_STR)
|
|
35
36
|
|
|
36
|
-
from hooks_constants.doc_gist_auto_publish_constants import ( # noqa: E402, I001
|
|
37
|
-
HOOK_SUBPROCESS_TIMEOUT_SECONDS as DOC_GIST_TIMEOUT_SECONDS,
|
|
38
|
-
)
|
|
39
37
|
from hooks_constants.post_tool_use_dispatcher_constants import ( # noqa: E402, I001
|
|
40
38
|
ALL_POST_HOSTED_HOOK_ENTRIES,
|
|
41
39
|
BLOCK_DECISION,
|
|
@@ -95,6 +93,26 @@ def _resolve_extra_arguments(each_entry: PostHostedHookEntry) -> list[str]:
|
|
|
95
93
|
return resolved_arguments
|
|
96
94
|
|
|
97
95
|
|
|
96
|
+
def _environment_without_git_hook_variables() -> dict[str, str]:
|
|
97
|
+
"""Return the process environment with git's hook-scoped variables removed.
|
|
98
|
+
|
|
99
|
+
Git exports variables such as GIT_DIR and GIT_INDEX_FILE to hook
|
|
100
|
+
processes. When this suite runs under a git hook (the pre-commit gate
|
|
101
|
+
replays staged test files), those variables leak into the hook
|
|
102
|
+
subprocesses and point their project discovery at the wrong checkout.
|
|
103
|
+
Stripping them restores the cwd-based discovery the hooks use in
|
|
104
|
+
production.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A copy of the environment without any GIT_-prefixed variable.
|
|
108
|
+
"""
|
|
109
|
+
return {
|
|
110
|
+
each_name: each_value
|
|
111
|
+
for each_name, each_value in os.environ.items()
|
|
112
|
+
if not each_name.startswith("GIT_")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
98
116
|
def _run_hook_subprocess(
|
|
99
117
|
each_entry: PostHostedHookEntry, payload_text: str
|
|
100
118
|
) -> subprocess.CompletedProcess[str]:
|
|
@@ -116,6 +134,7 @@ def _run_hook_subprocess(
|
|
|
116
134
|
capture_output=True,
|
|
117
135
|
text=True,
|
|
118
136
|
encoding="utf-8",
|
|
137
|
+
env=_environment_without_git_hook_variables(),
|
|
119
138
|
)
|
|
120
139
|
|
|
121
140
|
|
|
@@ -135,6 +154,7 @@ def _run_dispatcher(payload_text: str) -> subprocess.CompletedProcess[str]:
|
|
|
135
154
|
capture_output=True,
|
|
136
155
|
text=True,
|
|
137
156
|
encoding="utf-8",
|
|
157
|
+
env=_environment_without_git_hook_variables(),
|
|
138
158
|
)
|
|
139
159
|
|
|
140
160
|
|
|
@@ -267,10 +287,10 @@ def _post_tool_use_dispatcher_harness_timeout_seconds() -> int:
|
|
|
267
287
|
def _hosted_hooks_worst_case_internal_seconds() -> int:
|
|
268
288
|
"""Return the summed worst-case internal subprocess budget of the hosted hooks.
|
|
269
289
|
|
|
270
|
-
The
|
|
271
|
-
type-checker
|
|
272
|
-
|
|
273
|
-
|
|
290
|
+
The hosted hooks run sequentially in one dispatcher process. The
|
|
291
|
+
type-checker and the formatter (its slower JS path) each carry their own
|
|
292
|
+
internal subprocess timeout; their sum is the dispatcher process's
|
|
293
|
+
worst-case runtime when each hook runs to its own ceiling.
|
|
274
294
|
|
|
275
295
|
Returns:
|
|
276
296
|
The summed worst-case internal subprocess seconds across the hosted hooks.
|
|
@@ -281,21 +301,17 @@ def _hosted_hooks_worst_case_internal_seconds() -> int:
|
|
|
281
301
|
auto_formatter.PYTHON_FORMAT_TIMEOUT_SECONDS,
|
|
282
302
|
auto_formatter.JS_FORMAT_TIMEOUT_SECONDS,
|
|
283
303
|
)
|
|
284
|
-
return
|
|
285
|
-
mypy_validator.MYPY_TIMEOUT_SECONDS
|
|
286
|
-
+ slowest_formatter_seconds
|
|
287
|
-
+ DOC_GIST_TIMEOUT_SECONDS
|
|
288
|
-
)
|
|
304
|
+
return mypy_validator.MYPY_TIMEOUT_SECONDS + slowest_formatter_seconds
|
|
289
305
|
|
|
290
306
|
|
|
291
307
|
def test_dispatcher_harness_timeout_clears_summed_hosted_hook_budgets() -> None:
|
|
292
308
|
"""The dispatcher harness timeout exceeds the summed worst-case hosted-hook budget.
|
|
293
309
|
|
|
294
|
-
The
|
|
310
|
+
The hosted hooks run sequentially under one harness timeout. When the
|
|
295
311
|
harness timeout does not clear their summed worst-case internal budgets, a
|
|
296
312
|
near-full slow type-check run consumes the budget and the harness kills the
|
|
297
|
-
dispatcher before the formatter
|
|
298
|
-
|
|
313
|
+
dispatcher before the formatter gets its turn. The harness timeout must
|
|
314
|
+
exceed the sum so every hosted hook runs to completion.
|
|
299
315
|
"""
|
|
300
316
|
harness_timeout_seconds = _post_tool_use_dispatcher_harness_timeout_seconds()
|
|
301
317
|
summed_internal_seconds = _hosted_hooks_worst_case_internal_seconds()
|
|
@@ -303,7 +319,7 @@ def test_dispatcher_harness_timeout_clears_summed_hosted_hook_budgets() -> None:
|
|
|
303
319
|
"PostToolUse dispatcher harness timeout "
|
|
304
320
|
f"({harness_timeout_seconds}s) must exceed the summed worst-case internal "
|
|
305
321
|
f"budgets of the hosted hooks ({summed_internal_seconds}s), or a slow "
|
|
306
|
-
"type-check run starves the formatter
|
|
322
|
+
"type-check run starves the formatter."
|
|
307
323
|
)
|
|
308
324
|
|
|
309
325
|
|
|
@@ -315,14 +331,14 @@ def test_clean_edit_of_plain_text_allows() -> None:
|
|
|
315
331
|
|
|
316
332
|
|
|
317
333
|
def test_clean_write_of_nonexistent_path_allows() -> None:
|
|
318
|
-
"""Dispatcher allows a Write whose path does not exist (mypy
|
|
334
|
+
"""Dispatcher allows a Write whose path does not exist (mypy skips)."""
|
|
319
335
|
missing_path = str(_VALIDATION_DIR / "does_not_exist_dispatcher_probe.txt")
|
|
320
336
|
payload_text = _write_payload(missing_path, "hello world\n")
|
|
321
337
|
_assert_dispatcher_matches_individual_hooks(payload_text)
|
|
322
338
|
|
|
323
339
|
|
|
324
|
-
def
|
|
325
|
-
"""Dispatcher allows an Edit of an existing non-HTML file
|
|
340
|
+
def test_edit_of_non_html_allows() -> None:
|
|
341
|
+
"""Dispatcher allows an Edit of an existing non-HTML file."""
|
|
326
342
|
existing_path = str(Path(__file__).resolve())
|
|
327
343
|
payload_text = _edit_payload(existing_path, "old", "new")
|
|
328
344
|
_assert_dispatcher_matches_individual_hooks(payload_text)
|
|
@@ -433,10 +449,10 @@ def test_formatter_formats_only_untracked_write_and_never_blocks(tmp_path: Path)
|
|
|
433
449
|
def test_non_block_stdout_preserved_in_aggregator_allow_path() -> None:
|
|
434
450
|
"""Aggregator preserves non-block hook stdout on the allow path.
|
|
435
451
|
|
|
436
|
-
A side-effect hook
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
452
|
+
A side-effect hook writes informational text to stdout without emitting a
|
|
453
|
+
block decision. The aggregator must carry that text into
|
|
454
|
+
all_non_block_stdout so the dispatcher can write it to the real stdout on
|
|
455
|
+
the allow path.
|
|
440
456
|
"""
|
|
441
457
|
informational_text = "https://htmlpreview.github.io/?https://gist.github.com/abc/123"
|
|
442
458
|
all_results = [
|
|
@@ -459,7 +475,7 @@ def test_non_block_stdout_preserved_in_aggregator_allow_path() -> None:
|
|
|
459
475
|
def test_non_block_stdout_preserved_in_aggregator_block_path() -> None:
|
|
460
476
|
"""Aggregator preserves non-block hook stdout even when another hook blocks.
|
|
461
477
|
|
|
462
|
-
When mypy_validator blocks and
|
|
478
|
+
When mypy_validator blocks and a side-effect hook wrote informational
|
|
463
479
|
text, both the block reason and the informational text survive in the
|
|
464
480
|
aggregated decision so _emit_block_decision can forward both to stdout.
|
|
465
481
|
"""
|
|
@@ -512,9 +528,9 @@ def test_early_hook_crash_does_not_drop_later_blocking_hook_block() -> None:
|
|
|
512
528
|
def test_non_blocking_hook_crash_leaves_decision_allow() -> None:
|
|
513
529
|
"""A crash in a non-blocking hook does not change an allow to a block.
|
|
514
530
|
|
|
515
|
-
A side-effect hook such as auto_formatter
|
|
516
|
-
|
|
517
|
-
|
|
531
|
+
A side-effect hook such as auto_formatter carries is_blocking=False. Its
|
|
532
|
+
crash must not surface a blocking signal — the aggregated decision stays
|
|
533
|
+
allow.
|
|
518
534
|
"""
|
|
519
535
|
all_results = [
|
|
520
536
|
PostHostedHookResult(captured_stdout="", did_crash=True, is_blocking=False),
|
|
@@ -566,12 +582,12 @@ def test_block_path_emits_single_parseable_json_object_on_stdout() -> None:
|
|
|
566
582
|
)
|
|
567
583
|
try:
|
|
568
584
|
payload_text = _write_payload(str(type_error_file), type_error_file.read_text())
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
)
|
|
585
|
+
completed_direct = _run_hook_subprocess(ALL_POST_HOSTED_HOOK_ENTRIES[0], payload_text)
|
|
586
|
+
direct_block, _direct_reason = _parse_block_decision(completed_direct)
|
|
572
587
|
assert direct_block, (
|
|
573
588
|
"Precondition failed: mypy_validator did not block a real type error directly. "
|
|
574
|
-
"Is mypy installed and the file inside the git project
|
|
589
|
+
"Is mypy installed and the file inside the git project?\n"
|
|
590
|
+
f"stdout: {completed_direct.stdout!r}\nstderr: {completed_direct.stderr!r}"
|
|
575
591
|
)
|
|
576
592
|
|
|
577
593
|
dispatcher_result = _run_dispatcher(payload_text)
|