claude-dev-env 1.41.0 → 1.43.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 +8 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
- package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
- package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +124 -20
- package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
- package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
- package/_shared/pr-loop/scripts/preflight.py +13 -31
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +76 -33
- package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -2
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +5 -3
- package/agents/pr-description-writer.md +50 -140
- package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
- package/hooks/_gh_pr_author_swap_utils.py +1 -1
- package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
- package/hooks/blocking/code_rules_enforcer.py +217 -99
- package/hooks/blocking/code_rules_path_utils.py +8 -1
- package/hooks/blocking/destructive_command_blocker.py +1 -1
- package/hooks/blocking/es_exe_path_rewriter.py +7 -13
- package/hooks/blocking/gh_body_arg_blocker.py +6 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
- package/hooks/blocking/gh_pr_author_restore.py +5 -5
- package/hooks/blocking/hedging_language_blocker.py +4 -10
- package/hooks/blocking/md_path_exemptions.py +205 -0
- package/hooks/blocking/md_to_html_blocker.py +48 -20
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
- package/hooks/blocking/pr_description_enforcer.py +626 -41
- package/hooks/blocking/question_to_user_enforcer.py +4 -10
- package/hooks/blocking/state_description_blocker.py +6 -12
- package/hooks/blocking/tdd_enforcer.py +1 -1
- package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +3 -3
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
- package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
- package/hooks/blocking/test_hedging_language_blocker.py +2 -2
- package/hooks/blocking/test_md_to_html_blocker.py +463 -8
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
- package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
- package/hooks/blocking/windows_rmtree_blocker.py +5 -11
- package/hooks/diagnostic/hook_log_extractor.py +1 -1
- package/hooks/diagnostic/hook_log_init.py +1 -1
- package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
- package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
- package/hooks/diagnostic/test_hook_log_init.py +2 -2
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
- package/hooks/git-hooks/gate_utils.py +1 -1
- package/hooks/git-hooks/pre_commit.py +1 -1
- package/hooks/git-hooks/pre_push.py +1 -1
- package/hooks/git-hooks/test_config.py +5 -5
- package/hooks/git-hooks/test_pre_push.py +6 -6
- package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
- package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
- package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
- package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
- package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
- package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
- package/hooks/{config → hooks_constants}/test_messages.py +2 -6
- package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
- package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
- package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
- package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
- package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
- package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
- package/hooks/session/session_env_cleanup.py +4 -10
- package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
- package/hooks/session/test_untracked_repo_detector.py +2 -2
- package/hooks/session/untracked_repo_detector.py +6 -12
- package/hooks/test__gh_pr_author_swap_utils.py +1 -1
- package/hooks/validators/run_all_validators.py +16 -5
- package/hooks/validators/test_output_formatter.py +46 -0
- package/hooks/workflow/doc_gist_auto_publish.py +1 -1
- package/hooks/workflow/md_to_html_companion.py +8 -15
- package/hooks/workflow/test_md_to_html_companion.py +184 -23
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +1 -1
- package/rules/vault-context.md +1 -1
- package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
- package/scripts/setup_project_paths.py +49 -11
- package/scripts/sweep_empty_dirs.py +10 -1
- package/scripts/test_setup_project_paths.py +2 -2
- package/scripts/test_sweep_empty_dirs.py +2 -6
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +1 -1
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +110 -13
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
- package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +71 -25
- package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
- package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
- package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
- package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
- package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
- package/skills/doc-gist/SKILL.md +1 -1
- package/skills/doc-gist/scripts/gist_upload.py +1 -1
- package/skills/implement/SKILL.md +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
- package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
- package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
- package/skills/pr-converge/scripts/check_convergence.py +175 -29
- package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
- package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
- package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
- package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
- package/skills/refine/SKILL.md +257 -0
- package/skills/refine/templates/implementation-notes-template.html +56 -0
- package/skills/refine/templates/plan-template.md +60 -0
- package/skills/session-log/SKILL.md +98 -233
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
- package/hooks/config/pr_description_enforcer_constants.py +0 -19
- package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +0 -20
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
- /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
- /package/hooks/{config → hooks_constants}/__init__.py +0 -0
- /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
- /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
- /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
- /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/messages.py +0 -0
- /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
- /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
- /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
- /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
- /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
- /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
- /package/skills/{pr-converge/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
- /package/skills/{pr-converge/scripts/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
- /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
- /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
|
@@ -15,11 +15,11 @@ import subprocess
|
|
|
15
15
|
import sys
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
-
_pr_converge_dir = Path(__file__).
|
|
18
|
+
_pr_converge_dir = Path(__file__).absolute().parent.parent
|
|
19
19
|
if str(_pr_converge_dir) not in sys.path:
|
|
20
20
|
sys.path.insert(0, str(_pr_converge_dir))
|
|
21
21
|
|
|
22
|
-
from
|
|
22
|
+
from pr_converge_skill_constants.constants import (
|
|
23
23
|
COPILOT_LOGIN_FILTER_SUBSTRING,
|
|
24
24
|
GH_REVIEWS_PATH_TEMPLATE,
|
|
25
25
|
REVIEWS_PER_PAGE,
|
|
@@ -20,11 +20,11 @@ import subprocess
|
|
|
20
20
|
import sys
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
-
_pr_converge_dir = Path(__file__).
|
|
23
|
+
_pr_converge_dir = Path(__file__).absolute().parent.parent
|
|
24
24
|
if str(_pr_converge_dir) not in sys.path:
|
|
25
25
|
sys.path.insert(0, str(_pr_converge_dir))
|
|
26
26
|
|
|
27
|
-
from
|
|
27
|
+
from pr_converge_skill_constants.constants import (
|
|
28
28
|
EXIT_CODE_GH_ERROR,
|
|
29
29
|
GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE,
|
|
30
30
|
GH_ISSUE_COMMENT_CREATE_PATH_TEMPLATE,
|
|
File without changes
|
package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py
RENAMED
|
@@ -11,7 +11,7 @@ settings) live here.
|
|
|
11
11
|
import re
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from
|
|
14
|
+
from pr_converge_skill_constants.constants import ( # noqa: F401
|
|
15
15
|
ALL_BUGBOT_CHECK_RUN_ACTIVE_STATUSES,
|
|
16
16
|
ALL_CLAUDE_DIRTY_REVIEW_STATES,
|
|
17
17
|
ALL_COPILOT_DIRTY_REVIEW_STATES,
|
|
@@ -9,22 +9,9 @@ Run: python3 packages/claude-dev-env/skills/pr-converge/scripts/reflow_skill_md.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
import sys
|
|
13
12
|
import textwrap
|
|
14
|
-
from pathlib import Path
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
while script_directory in sys.path:
|
|
19
|
-
sys.path.remove(script_directory)
|
|
20
|
-
if script_directory not in sys.path:
|
|
21
|
-
sys.path.insert(0, script_directory)
|
|
22
|
-
|
|
23
|
-
from evict_cached_config_modules import evict_cached_config_modules
|
|
24
|
-
|
|
25
|
-
evict_cached_config_modules()
|
|
26
|
-
|
|
27
|
-
from config.reflow_skill_md_constants import (
|
|
14
|
+
from pr_converge_scripts_constants.reflow_skill_md_constants import (
|
|
28
15
|
BASH_CONTINUATION_MARKER_WIDTH,
|
|
29
16
|
BULLET_LIST_ITEM_PATTERN as BULLET_RE,
|
|
30
17
|
MARKDOWN_REFERENCE_DEFINITION_PATTERN as REF_DEF_RE,
|
|
@@ -36,6 +23,14 @@ from config.reflow_skill_md_constants import (
|
|
|
36
23
|
|
|
37
24
|
|
|
38
25
|
def wrap_paragraph_plain(text: str) -> list[str]:
|
|
26
|
+
"""Wrap a plain paragraph to MAX_WIDTH after collapsing whitespace.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
text: Paragraph text with internal whitespace runs to collapse.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Wrapped lines; empty list when the input collapses to nothing.
|
|
33
|
+
"""
|
|
39
34
|
collapsed = " ".join(text.split())
|
|
40
35
|
if not collapsed:
|
|
41
36
|
return []
|
|
@@ -48,6 +43,17 @@ def wrap_paragraph_plain(text: str) -> list[str]:
|
|
|
48
43
|
|
|
49
44
|
|
|
50
45
|
def wrap_list_item(lead_ws: str, marker: str, body: str) -> list[str]:
|
|
46
|
+
"""Wrap a list item, preserving the leading marker and indentation.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
lead_ws: Leading whitespace before the list marker.
|
|
50
|
+
marker: List marker such as a hyphen or numeric prefix.
|
|
51
|
+
body: Item body text to wrap.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Wrapped lines with the marker on the first line and matching
|
|
55
|
+
indentation on subsequent lines.
|
|
56
|
+
"""
|
|
51
57
|
collapsed = " ".join(body.split())
|
|
52
58
|
if not collapsed:
|
|
53
59
|
return [lead_ws + marker.rstrip()]
|
|
@@ -67,6 +73,16 @@ def reflow_yaml_description_block(
|
|
|
67
73
|
all_lines: list[str],
|
|
68
74
|
body_start: int,
|
|
69
75
|
) -> tuple[list[str], int]:
|
|
76
|
+
"""Reflow the YAML description block until the closing fence.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
all_lines: Full SKILL.md lines.
|
|
80
|
+
body_start: Index of the first description body line.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Tuple of wrapped description lines and the index just past the
|
|
84
|
+
closing fence.
|
|
85
|
+
"""
|
|
70
86
|
body_parts: list[str] = []
|
|
71
87
|
index = body_start
|
|
72
88
|
while index < len(all_lines):
|
|
@@ -95,6 +111,16 @@ def is_table_line(line: str) -> bool:
|
|
|
95
111
|
|
|
96
112
|
|
|
97
113
|
def is_new_logical_line(stripped: str) -> bool:
|
|
114
|
+
"""Decide whether ``stripped`` starts a new logical line.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
stripped: Candidate line with leading whitespace already removed.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True when the line begins a new markdown construct (fence, heading,
|
|
121
|
+
table row, list item, reference definition, or example tag) and
|
|
122
|
+
therefore must not be merged into the prior buffer.
|
|
123
|
+
"""
|
|
98
124
|
if not stripped:
|
|
99
125
|
return False
|
|
100
126
|
if stripped.startswith("```"):
|
|
@@ -115,7 +141,16 @@ def is_new_logical_line(stripped: str) -> bool:
|
|
|
115
141
|
|
|
116
142
|
|
|
117
143
|
def merge_without_space(buffer: str, continuation: str) -> bool:
|
|
118
|
-
"""Join without space only for split markdown link URL paths.
|
|
144
|
+
"""Join without space only for split markdown link URL paths.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
buffer: Accumulated line preceding the candidate continuation.
|
|
148
|
+
continuation: Next line to evaluate for joining.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True when continuation is the tail of a split markdown link target
|
|
152
|
+
and buffer ends inside an unfinished link target.
|
|
153
|
+
"""
|
|
119
154
|
base = buffer.rstrip()
|
|
120
155
|
stripped = continuation.lstrip()
|
|
121
156
|
if not base or not stripped:
|
|
@@ -126,6 +161,15 @@ def merge_without_space(buffer: str, continuation: str) -> bool:
|
|
|
126
161
|
|
|
127
162
|
|
|
128
163
|
def merge_soft_breaks(all_lines: list[str]) -> list[str]:
|
|
164
|
+
"""Merge soft line breaks across non-fence markdown paragraphs.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
all_lines: Raw SKILL.md lines.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Lines with each paragraph collapsed to a single buffer line; fences
|
|
171
|
+
and blank lines are preserved verbatim.
|
|
172
|
+
"""
|
|
129
173
|
reflowed_lines: list[str] = []
|
|
130
174
|
index = 0
|
|
131
175
|
is_inside_fence = False
|
|
@@ -166,6 +210,15 @@ def merge_soft_breaks(all_lines: list[str]) -> list[str]:
|
|
|
166
210
|
|
|
167
211
|
|
|
168
212
|
def reflow_merged_line(line: str) -> list[str]:
|
|
213
|
+
"""Reflow a single merged buffer line into MAX_WIDTH-bounded lines.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
line: Buffer line produced by merge_soft_breaks.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Wrapped lines; structural constructs (fences, tables, separators)
|
|
220
|
+
are returned unchanged.
|
|
221
|
+
"""
|
|
169
222
|
stripped = line.strip()
|
|
170
223
|
if stripped == "":
|
|
171
224
|
return [""]
|
|
@@ -221,6 +274,14 @@ def reflow_merged_line(line: str) -> list[str]:
|
|
|
221
274
|
|
|
222
275
|
|
|
223
276
|
def reflow_markdown_body(all_lines: list[str]) -> list[str]:
|
|
277
|
+
"""Merge soft breaks then reflow every line of the SKILL.md body.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
all_lines: Raw SKILL.md body lines following the YAML front matter.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Reflowed body lines bounded by MAX_WIDTH.
|
|
284
|
+
"""
|
|
224
285
|
merged = merge_soft_breaks(all_lines)
|
|
225
286
|
reflowed_lines: list[str] = []
|
|
226
287
|
for each_line in merged:
|
|
@@ -232,7 +293,15 @@ def reflow_markdown_body(all_lines: list[str]) -> list[str]:
|
|
|
232
293
|
|
|
233
294
|
|
|
234
295
|
def wrap_long_bash_fence_lines(all_lines: list[str]) -> list[str]:
|
|
235
|
-
"""Hard-wrap
|
|
296
|
+
"""Hard-wrap bash fence bodies that still exceed MAX_WIDTH.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
all_lines: SKILL.md body lines after paragraph reflow.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Lines with overlong bash-fence bodies split on whitespace with a
|
|
303
|
+
trailing backslash continuation marker.
|
|
304
|
+
"""
|
|
236
305
|
wrapped_lines: list[str] = []
|
|
237
306
|
is_inside_bash_fence = False
|
|
238
307
|
for each_line in all_lines:
|
|
@@ -272,6 +341,11 @@ def wrap_long_bash_fence_lines(all_lines: list[str]) -> list[str]:
|
|
|
272
341
|
|
|
273
342
|
|
|
274
343
|
def main() -> None:
|
|
344
|
+
"""Read SKILL.md, reflow it to MAX_WIDTH, and write the result back.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
SystemExit: When the file does not start with YAML front matter.
|
|
348
|
+
"""
|
|
275
349
|
raw = SKILL_PATH.read_text(encoding="utf-8")
|
|
276
350
|
lines = raw.splitlines()
|
|
277
351
|
if not lines or lines[0].strip() != "---":
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Tests for check_convergence.
|
|
2
|
+
|
|
3
|
+
Covers the bugteam audit gate (`_check_bugteam_clean`) which identifies
|
|
4
|
+
bugteam reviews by body header signature rather than by the posting user's
|
|
5
|
+
GitHub login. Three scenarios are exercised:
|
|
6
|
+
|
|
7
|
+
- a clean bugteam review on the current HEAD passes the gate
|
|
8
|
+
- a dirty bugteam review on the current HEAD fails the gate
|
|
9
|
+
- the absence of any bugteam review on the current HEAD fails the gate
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import Callable
|
|
20
|
+
|
|
21
|
+
import pytest
|
|
22
|
+
|
|
23
|
+
_SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
|
|
24
|
+
_PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
|
|
25
|
+
|
|
26
|
+
if str(_PR_CONVERGE_DIRECTORY) not in sys.path:
|
|
27
|
+
sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_module() -> ModuleType:
|
|
31
|
+
for each_cached_name in [
|
|
32
|
+
each_key
|
|
33
|
+
for each_key in list(sys.modules)
|
|
34
|
+
if each_key == "config" or each_key.startswith("config.")
|
|
35
|
+
]:
|
|
36
|
+
sys.modules.pop(each_cached_name, None)
|
|
37
|
+
if str(_PR_CONVERGE_DIRECTORY) in sys.path:
|
|
38
|
+
sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
|
|
39
|
+
sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
|
|
40
|
+
module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
|
|
41
|
+
spec = importlib.util.spec_from_file_location(
|
|
42
|
+
"check_convergence_under_test", module_path
|
|
43
|
+
)
|
|
44
|
+
assert spec is not None
|
|
45
|
+
assert spec.loader is not None
|
|
46
|
+
module = importlib.util.module_from_spec(spec)
|
|
47
|
+
spec.loader.exec_module(module)
|
|
48
|
+
return module
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
check_convergence = _load_module()
|
|
52
|
+
|
|
53
|
+
CURRENT_HEAD_SHA = "abcdef1234567890abcdef1234567890abcdef12"
|
|
54
|
+
OTHER_HEAD_SHA = "0000000000000000000000000000000000000000"
|
|
55
|
+
CLEAN_BUGTEAM_BODY = (
|
|
56
|
+
"**Bugteam audit completed** —— Clean — no findings\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"---\n"
|
|
59
|
+
"### Audit pass clean\n"
|
|
60
|
+
"\n"
|
|
61
|
+
"The Bugteam audit pass against commit `abcdef1` found no findings.\n"
|
|
62
|
+
)
|
|
63
|
+
DIRTY_BUGTEAM_BODY = (
|
|
64
|
+
"**Bugteam audit completed** —— Findings requested\n"
|
|
65
|
+
"\n"
|
|
66
|
+
"---\n"
|
|
67
|
+
"### Findings recorded as inline review comments\n"
|
|
68
|
+
"\n"
|
|
69
|
+
"The Bugteam audit pass against commit `abcdef1` surfaced 2 finding(s).\n"
|
|
70
|
+
)
|
|
71
|
+
NON_BUGTEAM_BODY = (
|
|
72
|
+
"Cursor Bugbot has reviewed your changes and found 0 potential issues."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_stub_gh_paginated(
|
|
77
|
+
all_review_objects: list[dict[str, object]],
|
|
78
|
+
) -> Callable[[str], tuple[int, str]]:
|
|
79
|
+
pages_payload = [all_review_objects]
|
|
80
|
+
serialized = json.dumps(pages_payload)
|
|
81
|
+
|
|
82
|
+
def stub_gh_api_paginated(endpoint_path: str) -> tuple[int, str]:
|
|
83
|
+
return 0, serialized
|
|
84
|
+
|
|
85
|
+
return stub_gh_api_paginated
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def should_pass_when_clean_bugteam_review_present_on_current_head(
|
|
89
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
90
|
+
) -> None:
|
|
91
|
+
reviews_payload = [
|
|
92
|
+
{
|
|
93
|
+
"id": 1001,
|
|
94
|
+
"body": CLEAN_BUGTEAM_BODY,
|
|
95
|
+
"commit_id": CURRENT_HEAD_SHA,
|
|
96
|
+
"submitted_at": "2026-05-17T12:00:00Z",
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
stub = _make_stub_gh_paginated(reviews_payload)
|
|
100
|
+
monkeypatch.setattr(check_convergence, "_gh_api_paginated", stub)
|
|
101
|
+
passed, detail = check_convergence._check_bugteam_clean(
|
|
102
|
+
owner="JonEcho",
|
|
103
|
+
repo="tests",
|
|
104
|
+
number=42,
|
|
105
|
+
head_sha=CURRENT_HEAD_SHA,
|
|
106
|
+
)
|
|
107
|
+
assert passed is True
|
|
108
|
+
assert "clean bugteam audit" in detail
|
|
109
|
+
assert CURRENT_HEAD_SHA[:7] in detail
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def should_fail_when_dirty_bugteam_review_present_on_current_head(
|
|
113
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
114
|
+
) -> None:
|
|
115
|
+
reviews_payload = [
|
|
116
|
+
{
|
|
117
|
+
"id": 1002,
|
|
118
|
+
"body": DIRTY_BUGTEAM_BODY,
|
|
119
|
+
"commit_id": CURRENT_HEAD_SHA,
|
|
120
|
+
"submitted_at": "2026-05-17T12:00:00Z",
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
stub = _make_stub_gh_paginated(reviews_payload)
|
|
124
|
+
monkeypatch.setattr(check_convergence, "_gh_api_paginated", stub)
|
|
125
|
+
passed, detail = check_convergence._check_bugteam_clean(
|
|
126
|
+
owner="JonEcho",
|
|
127
|
+
repo="tests",
|
|
128
|
+
number=42,
|
|
129
|
+
head_sha=CURRENT_HEAD_SHA,
|
|
130
|
+
)
|
|
131
|
+
assert passed is False
|
|
132
|
+
assert "dirty bugteam audit" in detail
|
|
133
|
+
assert CURRENT_HEAD_SHA[:7] in detail
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def should_fail_when_no_bugteam_review_present_on_current_head(
|
|
137
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
138
|
+
) -> None:
|
|
139
|
+
reviews_payload = [
|
|
140
|
+
{
|
|
141
|
+
"id": 1003,
|
|
142
|
+
"body": NON_BUGTEAM_BODY,
|
|
143
|
+
"commit_id": CURRENT_HEAD_SHA,
|
|
144
|
+
"submitted_at": "2026-05-17T12:00:00Z",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": 1004,
|
|
148
|
+
"body": CLEAN_BUGTEAM_BODY,
|
|
149
|
+
"commit_id": OTHER_HEAD_SHA,
|
|
150
|
+
"submitted_at": "2026-05-17T11:00:00Z",
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
stub = _make_stub_gh_paginated(reviews_payload)
|
|
154
|
+
monkeypatch.setattr(check_convergence, "_gh_api_paginated", stub)
|
|
155
|
+
passed, detail = check_convergence._check_bugteam_clean(
|
|
156
|
+
owner="JonEcho",
|
|
157
|
+
repo="tests",
|
|
158
|
+
number=42,
|
|
159
|
+
head_sha=CURRENT_HEAD_SHA,
|
|
160
|
+
)
|
|
161
|
+
assert passed is False
|
|
162
|
+
assert "no bugteam review found" in detail
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def should_fail_with_shape_detail_when_gh_returns_non_list_payload(
|
|
166
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
167
|
+
) -> None:
|
|
168
|
+
error_object_payload = {"message": "Not Found", "documentation_url": "https://docs.github.com/rest"}
|
|
169
|
+
serialized_error = json.dumps(error_object_payload)
|
|
170
|
+
|
|
171
|
+
def stub_gh_api_paginated_returning_object(endpoint_path: str) -> tuple[int, str]:
|
|
172
|
+
return 0, serialized_error
|
|
173
|
+
|
|
174
|
+
monkeypatch.setattr(
|
|
175
|
+
check_convergence, "_gh_api_paginated", stub_gh_api_paginated_returning_object
|
|
176
|
+
)
|
|
177
|
+
passed, detail = check_convergence._check_bugteam_clean(
|
|
178
|
+
owner="JonEcho",
|
|
179
|
+
repo="tests",
|
|
180
|
+
number=42,
|
|
181
|
+
head_sha=CURRENT_HEAD_SHA,
|
|
182
|
+
)
|
|
183
|
+
assert passed is False
|
|
184
|
+
assert "unexpected gh api response shape" in detail
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_private_helpers_recognize_clean_new_header_body() -> None:
|
|
188
|
+
assert check_convergence._is_bugteam_review(CLEAN_BUGTEAM_BODY) is True
|
|
189
|
+
assert check_convergence._is_clean_bugteam_review(CLEAN_BUGTEAM_BODY) is True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_private_helpers_recognize_dirty_new_header_body() -> None:
|
|
193
|
+
assert check_convergence._is_bugteam_review(DIRTY_BUGTEAM_BODY) is True
|
|
194
|
+
assert check_convergence._is_clean_bugteam_review(DIRTY_BUGTEAM_BODY) is False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_private_helpers_reject_non_bugteam_body() -> None:
|
|
198
|
+
assert check_convergence._is_bugteam_review(NON_BUGTEAM_BODY) is False
|
|
199
|
+
assert check_convergence._is_clean_bugteam_review(NON_BUGTEAM_BODY) is False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
CLEAN_LEGACY_BUGTEAM_BODY = (
|
|
203
|
+
"## /bugteam loop 1 audit: 0 P0 / 0 P1 / 0 P2 → clean"
|
|
204
|
+
)
|
|
205
|
+
DIRTY_LEGACY_BUGTEAM_BODY = (
|
|
206
|
+
"## /bugteam loop 1 audit: 1 P0 / 0 P1 / 0 P2 → dirty"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_private_helpers_recognize_clean_legacy_header_body() -> None:
|
|
211
|
+
assert check_convergence._is_bugteam_review(CLEAN_LEGACY_BUGTEAM_BODY) is True
|
|
212
|
+
assert check_convergence._is_clean_bugteam_review(CLEAN_LEGACY_BUGTEAM_BODY) is True
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_private_helpers_recognize_dirty_legacy_header_body() -> None:
|
|
216
|
+
assert check_convergence._is_bugteam_review(DIRTY_LEGACY_BUGTEAM_BODY) is True
|
|
217
|
+
assert check_convergence._is_clean_bugteam_review(DIRTY_LEGACY_BUGTEAM_BODY) is False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def should_bypass_bugbot_gates_when_bugbot_down_is_true(
|
|
221
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
222
|
+
) -> None:
|
|
223
|
+
all_invocation_names: list[str] = []
|
|
224
|
+
|
|
225
|
+
def stub_get_pr_head_sha(*, owner: str, repo: str, number: int) -> str:
|
|
226
|
+
all_invocation_names.append("_get_pr_head_sha")
|
|
227
|
+
return CURRENT_HEAD_SHA
|
|
228
|
+
|
|
229
|
+
def stub_check_bugbot_should_not_be_called(
|
|
230
|
+
*, owner: str, repo: str, sha: str
|
|
231
|
+
) -> tuple[bool, str]:
|
|
232
|
+
all_invocation_names.append("_check_bugbot")
|
|
233
|
+
raise AssertionError("_check_bugbot must not be invoked when bugbot_down=True")
|
|
234
|
+
|
|
235
|
+
def stub_check_bugbot_not_dirty_should_not_be_called(
|
|
236
|
+
*, owner: str, repo: str, number: int, head_sha: str
|
|
237
|
+
) -> tuple[bool, str]:
|
|
238
|
+
all_invocation_names.append("_check_bugbot_not_dirty")
|
|
239
|
+
raise AssertionError(
|
|
240
|
+
"_check_bugbot_not_dirty must not be invoked when bugbot_down=True"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def stub_check_bugteam_clean(
|
|
244
|
+
*, owner: str, repo: str, number: int, head_sha: str
|
|
245
|
+
) -> tuple[bool, str]:
|
|
246
|
+
all_invocation_names.append("_check_bugteam_clean")
|
|
247
|
+
return True, "stub passing"
|
|
248
|
+
|
|
249
|
+
def stub_check_bot_review(
|
|
250
|
+
*,
|
|
251
|
+
owner: str,
|
|
252
|
+
repo: str,
|
|
253
|
+
number: int,
|
|
254
|
+
head_sha: str,
|
|
255
|
+
login_substring: str,
|
|
256
|
+
clean_states: tuple[str, ...],
|
|
257
|
+
dirty_states: tuple[str, ...],
|
|
258
|
+
label: str,
|
|
259
|
+
) -> tuple[bool, str]:
|
|
260
|
+
all_invocation_names.append("_check_bot_review")
|
|
261
|
+
return True, "stub passing"
|
|
262
|
+
|
|
263
|
+
def stub_count_unresolved_bot_threads(
|
|
264
|
+
*, owner: str, repo: str, number: int
|
|
265
|
+
) -> tuple[bool, str]:
|
|
266
|
+
all_invocation_names.append("_count_unresolved_bot_threads")
|
|
267
|
+
return True, "stub passing"
|
|
268
|
+
|
|
269
|
+
def stub_get_mergeable(
|
|
270
|
+
*, owner: str, repo: str, number: int
|
|
271
|
+
) -> tuple[bool, str]:
|
|
272
|
+
all_invocation_names.append("_get_mergeable")
|
|
273
|
+
return True, "stub passing"
|
|
274
|
+
|
|
275
|
+
def stub_check_no_pending_reviews(
|
|
276
|
+
*, owner: str, repo: str, number: int
|
|
277
|
+
) -> tuple[bool, str]:
|
|
278
|
+
all_invocation_names.append("_check_no_pending_reviews")
|
|
279
|
+
return True, "stub passing"
|
|
280
|
+
|
|
281
|
+
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", stub_get_pr_head_sha)
|
|
282
|
+
monkeypatch.setattr(check_convergence, "_check_bugbot", stub_check_bugbot_should_not_be_called)
|
|
283
|
+
monkeypatch.setattr(
|
|
284
|
+
check_convergence,
|
|
285
|
+
"_check_bugbot_not_dirty",
|
|
286
|
+
stub_check_bugbot_not_dirty_should_not_be_called,
|
|
287
|
+
)
|
|
288
|
+
monkeypatch.setattr(check_convergence, "_check_bugteam_clean", stub_check_bugteam_clean)
|
|
289
|
+
monkeypatch.setattr(check_convergence, "_check_bot_review", stub_check_bot_review)
|
|
290
|
+
monkeypatch.setattr(
|
|
291
|
+
check_convergence, "_count_unresolved_bot_threads", stub_count_unresolved_bot_threads
|
|
292
|
+
)
|
|
293
|
+
monkeypatch.setattr(check_convergence, "_get_mergeable", stub_get_mergeable)
|
|
294
|
+
monkeypatch.setattr(
|
|
295
|
+
check_convergence, "_check_no_pending_reviews", stub_check_no_pending_reviews
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
exit_code = check_convergence.check_all(
|
|
299
|
+
owner="o", repo="r", number=1, bugbot_down=True
|
|
300
|
+
)
|
|
301
|
+
captured_stdout = capsys.readouterr().out
|
|
302
|
+
|
|
303
|
+
assert "_check_bugbot" not in all_invocation_names
|
|
304
|
+
assert "_check_bugbot_not_dirty" not in all_invocation_names
|
|
305
|
+
assert "bypassed (bugbot_down)" in captured_stdout
|
|
306
|
+
assert exit_code == 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def should_propagate_systemexit_from_get_pr_head_sha(
|
|
310
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
311
|
+
) -> None:
|
|
312
|
+
def stub_get_pr_head_sha_raising_systemexit(
|
|
313
|
+
*, owner: str, repo: str, number: int
|
|
314
|
+
) -> str:
|
|
315
|
+
raise SystemExit(check_convergence.EXIT_CODE_GH_ERROR)
|
|
316
|
+
|
|
317
|
+
monkeypatch.setattr(
|
|
318
|
+
check_convergence, "_get_pr_head_sha", stub_get_pr_head_sha_raising_systemexit
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
322
|
+
check_convergence.check_all(owner="o", repo="r", number=1, bugbot_down=False)
|
|
323
|
+
|
|
324
|
+
assert exc_info.value.code == check_convergence.EXIT_CODE_GH_ERROR
|
|
@@ -108,27 +108,6 @@ def test_reflow_merged_line_preserves_long_markdown_reference_definition() -> No
|
|
|
108
108
|
assert reflow_module.reflow_merged_line(line) == [stripped_line]
|
|
109
109
|
|
|
110
110
|
|
|
111
|
-
def test_reflow_bootstrap_moves_script_directory_ahead_of_shadow_config(
|
|
112
|
-
tmp_path: Path,
|
|
113
|
-
) -> None:
|
|
114
|
-
"""sys.path bootstrap must move the script directory ahead of shadow config packages."""
|
|
115
|
-
shadow_config_directory = tmp_path / "shadow" / "config"
|
|
116
|
-
shadow_config_directory.mkdir(parents=True)
|
|
117
|
-
(shadow_config_directory / "__init__.py").write_text("", encoding="utf-8")
|
|
118
|
-
(shadow_config_directory / "pr_converge_constants.py").write_text(
|
|
119
|
-
"BROKEN = True\n", encoding="utf-8"
|
|
120
|
-
)
|
|
121
|
-
original_sys_path = list(sys.path)
|
|
122
|
-
try:
|
|
123
|
-
sys.path.insert(0, str(tmp_path / "shadow"))
|
|
124
|
-
loaded_module = _load_module()
|
|
125
|
-
assert loaded_module.MAX_WIDTH == 80
|
|
126
|
-
assert sys.path[0] == str(_SCRIPTS_DIRECTORY)
|
|
127
|
-
assert sys.path.count(str(_SCRIPTS_DIRECTORY)) == 1
|
|
128
|
-
finally:
|
|
129
|
-
sys.path[:] = original_sys_path
|
|
130
|
-
|
|
131
|
-
|
|
132
111
|
def test_wrap_long_bash_fence_lines_uses_continuation_marker_for_long_lines() -> None:
|
|
133
112
|
"""Wrapped continuation lines use the bash continuation marker."""
|
|
134
113
|
long_line = "echo " + "word " * 20
|
|
@@ -150,13 +129,3 @@ def test_reflow_uses_config_constant_for_continuation_marker_width() -> None:
|
|
|
150
129
|
"reflow_skill_md.py must import BASH_CONTINUATION_MARKER_WIDTH from config"
|
|
151
130
|
)
|
|
152
131
|
|
|
153
|
-
def test_reflow_bootstrap_matches_code_rules_sys_path_pattern() -> None:
|
|
154
|
-
"""Bootstrap must guard insert with a membership check."""
|
|
155
|
-
module_path = _SCRIPTS_DIRECTORY / "reflow_skill_md.py"
|
|
156
|
-
source = module_path.read_text(encoding="utf-8")
|
|
157
|
-
assert "while script_directory in sys.path:" in source, (
|
|
158
|
-
"Bootstrap must dedup script_directory entries before insert"
|
|
159
|
-
)
|
|
160
|
-
assert "sys.path.insert(0, script_directory)" in source, (
|
|
161
|
-
"Bootstrap must insert script_directory at index 0"
|
|
162
|
-
)
|