claude-dev-env 1.34.1 → 1.36.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/agents/clean-coder.md +109 -1
- package/agents/docs-agent.md +1 -1
- package/agents/project-docs-analyzer.md +0 -1
- package/agents/skill-to-agent-converter.md +0 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/commands/initialize.md +0 -1
- package/commands/readability-review.md +4 -4
- package/commands/review-plan.md +2 -4
- package/commands/stubcheck.md +1 -2
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +686 -60
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +273 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +93 -125
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +94 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +576 -95
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +47 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/agents/agent-writer.md +0 -157
- package/agents/config-centralizer.md +0 -686
- package/agents/config-extraction-agent.md +0 -225
- package/agents/doc-orchestrator.md +0 -47
- package/agents/docx-agent.md +0 -211
- package/agents/magic-value-eliminator-agent.md +0 -72
- package/agents/mandatory-agent-workflow-agent.md +0 -88
- package/agents/parallel-workflow-coordinator.md +0 -779
- package/agents/pdf-agent.md +0 -302
- package/agents/project-context-loader.md +0 -238
- package/agents/readability-review-agent.md +0 -76
- package/agents/refactoring-specialist.md +0 -69
- package/agents/right-sized-engineer.md +0 -129
- package/agents/session-continuity-manager.md +0 -53
- package/agents/stub-detector-agent.md +0 -140
- package/agents/tdd-test-writer.md +0 -62
- package/agents/test-data-builder.md +0 -68
- package/agents/tooling-builder.md +0 -78
- package/agents/validation-expert.md +0 -71
- package/agents/xlsx-agent.md +0 -169
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tests for mark_pr_ready.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- gh pr ready is invoked with the PR number and --repo flag
|
|
5
|
+
- subprocess errors propagate
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_module() -> ModuleType:
|
|
20
|
+
module_path = Path(__file__).parent / "mark_pr_ready.py"
|
|
21
|
+
spec = importlib.util.spec_from_file_location("mark_pr_ready", module_path)
|
|
22
|
+
assert spec is not None
|
|
23
|
+
assert spec.loader is not None
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
return module
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
mark_pr_ready_module = _load_module()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _completed(stdout: str) -> subprocess.CompletedProcess:
|
|
33
|
+
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
34
|
+
process.stdout = stdout
|
|
35
|
+
process.returncode = 0
|
|
36
|
+
return process
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_invoke_gh_pr_ready_with_number_and_repo() -> None:
|
|
40
|
+
with patch("subprocess.run") as mock_run:
|
|
41
|
+
mock_run.return_value = _completed('Pull request "#42" is marked as ready')
|
|
42
|
+
mark_pr_ready_module.mark_pr_ready(owner="acme", repo="widget", number=42)
|
|
43
|
+
invoked_argv = mock_run.call_args[0][0]
|
|
44
|
+
assert invoked_argv[0:3] == ["gh", "pr", "ready"]
|
|
45
|
+
assert "42" in invoked_argv
|
|
46
|
+
assert "--repo" in invoked_argv
|
|
47
|
+
assert "acme/widget" in invoked_argv
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_raise_when_gh_subprocess_fails() -> None:
|
|
51
|
+
failure = subprocess.CalledProcessError(
|
|
52
|
+
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
53
|
+
)
|
|
54
|
+
with patch("subprocess.run", side_effect=failure):
|
|
55
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
56
|
+
mark_pr_ready_module.mark_pr_ready(owner="acme", repo="widget", number=42)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_render_repo_arg_via_named_template_constant() -> None:
|
|
60
|
+
with patch("subprocess.run") as mock_run:
|
|
61
|
+
mock_run.return_value = _completed("ok\n")
|
|
62
|
+
mark_pr_ready_module.mark_pr_ready(owner="acme", repo="widget", number=42)
|
|
63
|
+
invoked_argv = mock_run.call_args[0][0]
|
|
64
|
+
expected_repo_arg = mark_pr_ready_module.GH_REPO_ARG_TEMPLATE.format(
|
|
65
|
+
owner="acme", repo="widget"
|
|
66
|
+
)
|
|
67
|
+
assert expected_repo_arg == "acme/widget"
|
|
68
|
+
repo_flag_index = invoked_argv.index("--repo")
|
|
69
|
+
assert invoked_argv[repo_flag_index + 1] == expected_repo_arg
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Tests for open_followup_copilot_pr.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- branch name follows COPILOT_FOLLOWUP_BRANCH_TEMPLATE with the short SHA
|
|
5
|
+
- subprocess sequence: gh pr view (base ref) -> git fetch -> git switch -c -> git push -> gh pr create
|
|
6
|
+
- gh pr create uses --draft, --base, --head, --title, --body-file (per gh-body-file rule)
|
|
7
|
+
- the returned PR URL is the trimmed stdout from gh pr create
|
|
8
|
+
- subprocess errors propagate
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
from unittest.mock import MagicMock, patch
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_module() -> ModuleType:
|
|
24
|
+
module_path = Path(__file__).parent / "open_followup_copilot_pr.py"
|
|
25
|
+
spec = importlib.util.spec_from_file_location(
|
|
26
|
+
"open_followup_copilot_pr", module_path
|
|
27
|
+
)
|
|
28
|
+
assert spec is not None
|
|
29
|
+
assert spec.loader is not None
|
|
30
|
+
module = importlib.util.module_from_spec(spec)
|
|
31
|
+
spec.loader.exec_module(module)
|
|
32
|
+
return module
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
open_followup_copilot_pr_module = _load_module()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _completed(stdout: str) -> subprocess.CompletedProcess:
|
|
39
|
+
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
40
|
+
process.stdout = stdout
|
|
41
|
+
process.returncode = 0
|
|
42
|
+
return process
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _scripted_subprocess_runs(
|
|
46
|
+
*,
|
|
47
|
+
base_ref_payload: str,
|
|
48
|
+
new_pr_url: str,
|
|
49
|
+
) -> list[subprocess.CompletedProcess]:
|
|
50
|
+
return [
|
|
51
|
+
_completed(base_ref_payload),
|
|
52
|
+
_completed(""),
|
|
53
|
+
_completed(""),
|
|
54
|
+
_completed(""),
|
|
55
|
+
_completed(new_pr_url),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_build_branch_name_from_parent_number_and_short_sha(
|
|
60
|
+
tmp_path: Path,
|
|
61
|
+
) -> None:
|
|
62
|
+
findings_file = tmp_path / "findings.md"
|
|
63
|
+
findings_file.write_text("- Item 1\n", encoding="utf-8")
|
|
64
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
65
|
+
base_ref_payload=json.dumps({"baseRefName": "main"}),
|
|
66
|
+
new_pr_url="https://github.com/acme/widget/pull/313\n",
|
|
67
|
+
)
|
|
68
|
+
with patch("subprocess.run", side_effect=payload_sequence) as mock_run:
|
|
69
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
70
|
+
owner="acme",
|
|
71
|
+
repo="widget",
|
|
72
|
+
parent_number=312,
|
|
73
|
+
head="abc12345deadbeefcafe",
|
|
74
|
+
findings_file=findings_file,
|
|
75
|
+
)
|
|
76
|
+
git_switch_argv = mock_run.call_args_list[2][0][0]
|
|
77
|
+
expected_branch = "chore/copilot-followup-312-abc12345"
|
|
78
|
+
assert expected_branch in git_switch_argv
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_invoke_subprocess_call_sequence_in_documented_order(
|
|
82
|
+
tmp_path: Path,
|
|
83
|
+
) -> None:
|
|
84
|
+
findings_file = tmp_path / "findings.md"
|
|
85
|
+
findings_file.write_text("- Item\n", encoding="utf-8")
|
|
86
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
87
|
+
base_ref_payload=json.dumps({"baseRefName": "main"}),
|
|
88
|
+
new_pr_url="https://github.com/acme/widget/pull/313\n",
|
|
89
|
+
)
|
|
90
|
+
with patch("subprocess.run", side_effect=payload_sequence) as mock_run:
|
|
91
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
92
|
+
owner="acme",
|
|
93
|
+
repo="widget",
|
|
94
|
+
parent_number=312,
|
|
95
|
+
head="abc12345deadbeefcafe",
|
|
96
|
+
findings_file=findings_file,
|
|
97
|
+
)
|
|
98
|
+
invoked_command_sequence = [
|
|
99
|
+
each_call[0][0] for each_call in mock_run.call_args_list
|
|
100
|
+
]
|
|
101
|
+
assert invoked_command_sequence[0][0:3] == ["gh", "pr", "view"]
|
|
102
|
+
assert invoked_command_sequence[1][0:2] == ["git", "fetch"]
|
|
103
|
+
assert invoked_command_sequence[2][0:3] == ["git", "switch", "-c"]
|
|
104
|
+
assert invoked_command_sequence[3][0:2] == ["git", "push"]
|
|
105
|
+
assert invoked_command_sequence[4][0:3] == ["gh", "pr", "create"]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_should_invoke_gh_pr_create_with_draft_and_body_file_flags(
|
|
109
|
+
tmp_path: Path,
|
|
110
|
+
) -> None:
|
|
111
|
+
findings_file = tmp_path / "findings.md"
|
|
112
|
+
findings_file.write_text("- Finding A\n", encoding="utf-8")
|
|
113
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
114
|
+
base_ref_payload=json.dumps({"baseRefName": "develop"}),
|
|
115
|
+
new_pr_url="https://github.com/acme/widget/pull/444\n",
|
|
116
|
+
)
|
|
117
|
+
with patch("subprocess.run", side_effect=payload_sequence) as mock_run:
|
|
118
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
119
|
+
owner="acme",
|
|
120
|
+
repo="widget",
|
|
121
|
+
parent_number=312,
|
|
122
|
+
head="abc12345deadbeef",
|
|
123
|
+
findings_file=findings_file,
|
|
124
|
+
)
|
|
125
|
+
pr_create_argv = mock_run.call_args_list[4][0][0]
|
|
126
|
+
assert pr_create_argv[0:3] == ["gh", "pr", "create"]
|
|
127
|
+
assert "--draft" in pr_create_argv
|
|
128
|
+
assert "--base" in pr_create_argv
|
|
129
|
+
assert "develop" in pr_create_argv
|
|
130
|
+
assert "--head" in pr_create_argv
|
|
131
|
+
assert "--title" in pr_create_argv
|
|
132
|
+
assert "--body-file" in pr_create_argv
|
|
133
|
+
body_file_argv = pr_create_argv[pr_create_argv.index("--body-file") + 1]
|
|
134
|
+
assert body_file_argv == str(findings_file)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_should_render_pr_title_via_named_template_constant(tmp_path: Path) -> None:
|
|
138
|
+
findings_file = tmp_path / "findings.md"
|
|
139
|
+
findings_file.write_text("- Finding\n", encoding="utf-8")
|
|
140
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
141
|
+
base_ref_payload=json.dumps({"baseRefName": "main"}),
|
|
142
|
+
new_pr_url="https://github.com/acme/widget/pull/513\n",
|
|
143
|
+
)
|
|
144
|
+
with patch("subprocess.run", side_effect=payload_sequence) as mock_run:
|
|
145
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
146
|
+
owner="acme",
|
|
147
|
+
repo="widget",
|
|
148
|
+
parent_number=312,
|
|
149
|
+
head="abc12345deadbeef",
|
|
150
|
+
findings_file=findings_file,
|
|
151
|
+
)
|
|
152
|
+
pr_create_argv = mock_run.call_args_list[4][0][0]
|
|
153
|
+
title_index = pr_create_argv.index("--title")
|
|
154
|
+
title_value = pr_create_argv[title_index + 1]
|
|
155
|
+
assert title_value == "chore: address Copilot findings from PR #312"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_should_return_trimmed_pr_url(tmp_path: Path) -> None:
|
|
159
|
+
findings_file = tmp_path / "findings.md"
|
|
160
|
+
findings_file.write_text("- Finding\n", encoding="utf-8")
|
|
161
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
162
|
+
base_ref_payload=json.dumps({"baseRefName": "main"}),
|
|
163
|
+
new_pr_url=" https://github.com/acme/widget/pull/313 \n",
|
|
164
|
+
)
|
|
165
|
+
with patch("subprocess.run", side_effect=payload_sequence):
|
|
166
|
+
new_pr_url = open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
167
|
+
owner="acme",
|
|
168
|
+
repo="widget",
|
|
169
|
+
parent_number=312,
|
|
170
|
+
head="abc12345deadbeef",
|
|
171
|
+
findings_file=findings_file,
|
|
172
|
+
)
|
|
173
|
+
assert new_pr_url == "https://github.com/acme/widget/pull/313"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_should_pass_repo_arg_to_gh_pr_view_for_base_ref(tmp_path: Path) -> None:
|
|
177
|
+
findings_file = tmp_path / "findings.md"
|
|
178
|
+
findings_file.write_text("- Finding\n", encoding="utf-8")
|
|
179
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
180
|
+
base_ref_payload=json.dumps({"baseRefName": "main"}),
|
|
181
|
+
new_pr_url="https://github.com/acme/widget/pull/313\n",
|
|
182
|
+
)
|
|
183
|
+
with patch("subprocess.run", side_effect=payload_sequence) as mock_run:
|
|
184
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
185
|
+
owner="acme",
|
|
186
|
+
repo="widget",
|
|
187
|
+
parent_number=312,
|
|
188
|
+
head="abc12345deadbeef",
|
|
189
|
+
findings_file=findings_file,
|
|
190
|
+
)
|
|
191
|
+
pr_view_argv = mock_run.call_args_list[0][0][0]
|
|
192
|
+
assert pr_view_argv[0:3] == ["gh", "pr", "view"]
|
|
193
|
+
assert "--repo" in pr_view_argv
|
|
194
|
+
repo_arg_value = pr_view_argv[pr_view_argv.index("--repo") + 1]
|
|
195
|
+
assert repo_arg_value == "acme/widget"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_pass_repo_arg_to_gh_pr_create_for_followup_pr(
|
|
199
|
+
tmp_path: Path,
|
|
200
|
+
) -> None:
|
|
201
|
+
findings_file = tmp_path / "findings.md"
|
|
202
|
+
findings_file.write_text("- Finding\n", encoding="utf-8")
|
|
203
|
+
payload_sequence = _scripted_subprocess_runs(
|
|
204
|
+
base_ref_payload=json.dumps({"baseRefName": "main"}),
|
|
205
|
+
new_pr_url="https://github.com/acme/widget/pull/313\n",
|
|
206
|
+
)
|
|
207
|
+
with patch("subprocess.run", side_effect=payload_sequence) as mock_run:
|
|
208
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
209
|
+
owner="acme",
|
|
210
|
+
repo="widget",
|
|
211
|
+
parent_number=312,
|
|
212
|
+
head="abc12345deadbeef",
|
|
213
|
+
findings_file=findings_file,
|
|
214
|
+
)
|
|
215
|
+
pr_create_argv = mock_run.call_args_list[4][0][0]
|
|
216
|
+
assert pr_create_argv[0:3] == ["gh", "pr", "create"]
|
|
217
|
+
assert "--repo" in pr_create_argv
|
|
218
|
+
repo_arg_value = pr_create_argv[pr_create_argv.index("--repo") + 1]
|
|
219
|
+
assert repo_arg_value == "acme/widget"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_should_raise_when_subprocess_fails(tmp_path: Path) -> None:
|
|
223
|
+
findings_file = tmp_path / "findings.md"
|
|
224
|
+
findings_file.write_text("- Finding\n", encoding="utf-8")
|
|
225
|
+
failure = subprocess.CalledProcessError(
|
|
226
|
+
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
227
|
+
)
|
|
228
|
+
with patch("subprocess.run", side_effect=failure):
|
|
229
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
230
|
+
open_followup_copilot_pr_module.open_followup_copilot_pr(
|
|
231
|
+
owner="acme",
|
|
232
|
+
repo="widget",
|
|
233
|
+
parent_number=312,
|
|
234
|
+
head="abc12345",
|
|
235
|
+
findings_file=findings_file,
|
|
236
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Tests for post-bugbot-run PowerShell helpers.
|
|
2
|
+
|
|
3
|
+
Covers Resolve-InvocationMode / Build-GhArgumentList for URL, owner/repo#N, and
|
|
4
|
+
-Repository/-Number forms (Windows Bugbot re-trigger argv construction).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _run_powershell(*, expression: str) -> str:
|
|
19
|
+
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
20
|
+
command = (
|
|
21
|
+
f". '{helpers}'; "
|
|
22
|
+
+ expression
|
|
23
|
+
+ " | ConvertTo-Json -Compress -Depth 5"
|
|
24
|
+
)
|
|
25
|
+
completed = subprocess.run(
|
|
26
|
+
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
errors="replace",
|
|
31
|
+
check=False,
|
|
32
|
+
)
|
|
33
|
+
if completed.returncode != 0:
|
|
34
|
+
raise AssertionError(
|
|
35
|
+
f"pwsh failed ({completed.returncode}): stderr={completed.stderr!r} stdout={completed.stdout!r}"
|
|
36
|
+
)
|
|
37
|
+
return completed.stdout.strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _argv_for(*, pull: str, repository: str, number: int, body: str) -> list[str]:
|
|
41
|
+
pull_esc = pull.replace("'", "''")
|
|
42
|
+
repository_esc = repository.replace("'", "''")
|
|
43
|
+
body_esc = body.replace("'", "''")
|
|
44
|
+
expression = (
|
|
45
|
+
f"$i = Resolve-InvocationMode -PullRequestInput '{pull_esc}' "
|
|
46
|
+
f"-RepositoryInput '{repository_esc}' -NumberInput {int(number)}; "
|
|
47
|
+
f"@(Build-GhArgumentList -Invocation $i -BodyFilePath '{body_esc}')"
|
|
48
|
+
)
|
|
49
|
+
raw = _run_powershell(expression=expression)
|
|
50
|
+
return json.loads(raw)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_build_arguments_for_https_pull_url() -> None:
|
|
54
|
+
argv = _argv_for(
|
|
55
|
+
pull="https://github.com/acme/widget/pull/42",
|
|
56
|
+
repository="",
|
|
57
|
+
number=0,
|
|
58
|
+
body=r"C:\\temp\\body.md",
|
|
59
|
+
)
|
|
60
|
+
assert argv == [
|
|
61
|
+
"pr",
|
|
62
|
+
"comment",
|
|
63
|
+
"https://github.com/acme/widget/pull/42",
|
|
64
|
+
"--body-file",
|
|
65
|
+
r"C:\\temp\\body.md",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_build_arguments_for_owner_repo_hash_form() -> None:
|
|
70
|
+
argv = _argv_for(
|
|
71
|
+
pull="acme/widget#7",
|
|
72
|
+
repository="",
|
|
73
|
+
number=0,
|
|
74
|
+
body=r"D:\\x\\f.md",
|
|
75
|
+
)
|
|
76
|
+
assert argv == ["pr", "comment", "7", "-R", "acme/widget", "--body-file", r"D:\\x\\f.md"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_should_build_arguments_for_repository_and_number_parameters() -> None:
|
|
80
|
+
argv = _argv_for(
|
|
81
|
+
pull="",
|
|
82
|
+
repository="jl-cmd/claude-code-config",
|
|
83
|
+
number=331,
|
|
84
|
+
body=r"E:\\y\\z.md",
|
|
85
|
+
)
|
|
86
|
+
assert argv == [
|
|
87
|
+
"pr",
|
|
88
|
+
"comment",
|
|
89
|
+
"331",
|
|
90
|
+
"-R",
|
|
91
|
+
"jl-cmd/claude-code-config",
|
|
92
|
+
"--body-file",
|
|
93
|
+
r"E:\\y\\z.md",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_should_fail_when_number_without_repository() -> None:
|
|
98
|
+
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
99
|
+
command = (
|
|
100
|
+
f". '{helpers}'; "
|
|
101
|
+
"try { Resolve-InvocationMode -PullRequestInput '' -RepositoryInput '' -NumberInput 3 } "
|
|
102
|
+
"catch { $_.Exception.Message }"
|
|
103
|
+
)
|
|
104
|
+
completed = subprocess.run(
|
|
105
|
+
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
encoding="utf-8",
|
|
109
|
+
errors="replace",
|
|
110
|
+
check=False,
|
|
111
|
+
)
|
|
112
|
+
assert completed.returncode == 0
|
|
113
|
+
message = completed.stdout.strip()
|
|
114
|
+
assert "Repository" in message
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_should_reject_pull_url_with_trailing_junk() -> None:
|
|
118
|
+
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
119
|
+
pull = "https://github.com/acme/widget/pull/42extra"
|
|
120
|
+
command = (
|
|
121
|
+
f". '{helpers}'; "
|
|
122
|
+
f"$i = Resolve-InvocationMode -PullRequestInput '{pull}' -RepositoryInput '' -NumberInput 0; "
|
|
123
|
+
r"try { Build-GhArgumentList -Invocation $i -BodyFilePath 'C:\\t\\b.md' } "
|
|
124
|
+
"catch { $_.Exception.Message }"
|
|
125
|
+
)
|
|
126
|
+
completed = subprocess.run(
|
|
127
|
+
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=True,
|
|
130
|
+
encoding="utf-8",
|
|
131
|
+
errors="replace",
|
|
132
|
+
check=False,
|
|
133
|
+
)
|
|
134
|
+
assert completed.returncode == 0
|
|
135
|
+
assert "Unrecognized PullRequest" in completed.stdout
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_fail_for_unrecognized_pull_string() -> None:
|
|
139
|
+
helpers = Path(__file__).resolve().parent / "post-bugbot-run.helpers.ps1"
|
|
140
|
+
pull = "not-a-valid-pr-reference"
|
|
141
|
+
command = (
|
|
142
|
+
f". '{helpers}'; "
|
|
143
|
+
f"$i = Resolve-InvocationMode -PullRequestInput '{pull}' -RepositoryInput '' -NumberInput 0; "
|
|
144
|
+
r"try { Build-GhArgumentList -Invocation $i -BodyFilePath 'C:\\t\\b.md' } "
|
|
145
|
+
"catch { $_.Exception.Message }"
|
|
146
|
+
)
|
|
147
|
+
completed = subprocess.run(
|
|
148
|
+
["pwsh", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command],
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
encoding="utf-8",
|
|
152
|
+
errors="replace",
|
|
153
|
+
check=False,
|
|
154
|
+
)
|
|
155
|
+
assert completed.returncode == 0
|
|
156
|
+
assert "Unrecognized PullRequest" in completed.stdout
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_full_script_removes_temp_files_when_gh_stub_succeeds(tmp_path: Path) -> None:
|
|
160
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
161
|
+
script_path = scripts_dir / "post-bugbot-run.ps1"
|
|
162
|
+
stub_bin_dir = tmp_path / "gh_stub_bin"
|
|
163
|
+
stub_bin_dir.mkdir()
|
|
164
|
+
gh_cmd = stub_bin_dir / "gh.cmd"
|
|
165
|
+
gh_cmd.write_text("@echo off\r\nexit /b 0\r\n", encoding="utf-8")
|
|
166
|
+
env = dict(os.environ)
|
|
167
|
+
env["PATH"] = str(stub_bin_dir) + os.pathsep + env.get("PATH", "")
|
|
168
|
+
completed = subprocess.run(
|
|
169
|
+
[
|
|
170
|
+
"pwsh",
|
|
171
|
+
"-NoProfile",
|
|
172
|
+
"-NonInteractive",
|
|
173
|
+
"-ExecutionPolicy",
|
|
174
|
+
"Bypass",
|
|
175
|
+
"-File",
|
|
176
|
+
str(script_path),
|
|
177
|
+
"acme/widget#9",
|
|
178
|
+
],
|
|
179
|
+
capture_output=True,
|
|
180
|
+
text=True,
|
|
181
|
+
encoding="utf-8",
|
|
182
|
+
errors="replace",
|
|
183
|
+
env=env,
|
|
184
|
+
check=False,
|
|
185
|
+
)
|
|
186
|
+
assert completed.returncode == 0, (completed.stdout, completed.stderr)
|
|
187
|
+
|
|
188
|
+
def test_post_bugbot_run_script_finally_removes_temp_paths() -> None:
|
|
189
|
+
script_text = (Path(__file__).resolve().parent / "post-bugbot-run.ps1").read_text(
|
|
190
|
+
encoding="utf-8"
|
|
191
|
+
)
|
|
192
|
+
assert "finally" in script_text
|
|
193
|
+
assert "Remove-Item" in script_text
|
|
194
|
+
assert "body_file_path" in script_text
|
|
195
|
+
assert "scratch_temp_path" in script_text
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Tests for reply_to_inline_comment.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- gh api -X POST is invoked against the inline-comments replies endpoint
|
|
5
|
+
- the reply body comes from a file via gh's `body=@<path>` form (per gh-body-file rule)
|
|
6
|
+
- the reply id from gh's JSON output is returned
|
|
7
|
+
- subprocess errors propagate
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
from unittest.mock import MagicMock, patch
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_module() -> ModuleType:
|
|
23
|
+
module_path = Path(__file__).parent / "reply_to_inline_comment.py"
|
|
24
|
+
spec = importlib.util.spec_from_file_location(
|
|
25
|
+
"reply_to_inline_comment", module_path
|
|
26
|
+
)
|
|
27
|
+
assert spec is not None
|
|
28
|
+
assert spec.loader is not None
|
|
29
|
+
module = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(module)
|
|
31
|
+
return module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
reply_to_inline_comment_module = _load_module()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _completed(stdout: str) -> subprocess.CompletedProcess:
|
|
38
|
+
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
39
|
+
process.stdout = stdout
|
|
40
|
+
process.returncode = 0
|
|
41
|
+
return process
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_invoke_gh_api_post_against_replies_endpoint(tmp_path: Path) -> None:
|
|
45
|
+
body_file = tmp_path / "reply.md"
|
|
46
|
+
body_file.write_text("Confirmed and fixed.\n", encoding="utf-8")
|
|
47
|
+
with patch("subprocess.run") as mock_run:
|
|
48
|
+
mock_run.return_value = _completed(json.dumps({"id": 7777}))
|
|
49
|
+
reply_to_inline_comment_module.reply_to_inline_comment(
|
|
50
|
+
owner="acme",
|
|
51
|
+
repo="widget",
|
|
52
|
+
number=42,
|
|
53
|
+
comment_id=12345,
|
|
54
|
+
body_file=body_file,
|
|
55
|
+
)
|
|
56
|
+
invoked_argv = mock_run.call_args[0][0]
|
|
57
|
+
assert invoked_argv[0:2] == ["gh", "api"]
|
|
58
|
+
assert "-X" in invoked_argv
|
|
59
|
+
assert "POST" in invoked_argv
|
|
60
|
+
assert "repos/acme/widget/pulls/42/comments/12345/replies" in invoked_argv
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_pass_body_via_field_at_path_form(tmp_path: Path) -> None:
|
|
64
|
+
body_file = tmp_path / "reply.md"
|
|
65
|
+
body_file.write_text("Confirmed and fixed.\n", encoding="utf-8")
|
|
66
|
+
with patch("subprocess.run") as mock_run:
|
|
67
|
+
mock_run.return_value = _completed(json.dumps({"id": 7777}))
|
|
68
|
+
reply_to_inline_comment_module.reply_to_inline_comment(
|
|
69
|
+
owner="acme",
|
|
70
|
+
repo="widget",
|
|
71
|
+
number=42,
|
|
72
|
+
comment_id=12345,
|
|
73
|
+
body_file=body_file,
|
|
74
|
+
)
|
|
75
|
+
invoked_argv = mock_run.call_args[0][0]
|
|
76
|
+
assert "-F" in invoked_argv
|
|
77
|
+
field_value = invoked_argv[invoked_argv.index("-F") + 1]
|
|
78
|
+
assert field_value.startswith("body=@")
|
|
79
|
+
assert str(body_file) in field_value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_should_return_reply_id_from_gh_response(tmp_path: Path) -> None:
|
|
83
|
+
body_file = tmp_path / "reply.md"
|
|
84
|
+
body_file.write_text("Reply text\n", encoding="utf-8")
|
|
85
|
+
with patch("subprocess.run") as mock_run:
|
|
86
|
+
mock_run.return_value = _completed(json.dumps({"id": 7777, "body": "..."}))
|
|
87
|
+
reply_id = reply_to_inline_comment_module.reply_to_inline_comment(
|
|
88
|
+
owner="acme",
|
|
89
|
+
repo="widget",
|
|
90
|
+
number=42,
|
|
91
|
+
comment_id=12345,
|
|
92
|
+
body_file=body_file,
|
|
93
|
+
)
|
|
94
|
+
assert reply_id == 7777
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_should_raise_when_gh_subprocess_fails(tmp_path: Path) -> None:
|
|
98
|
+
body_file = tmp_path / "reply.md"
|
|
99
|
+
body_file.write_text("Reply text\n", encoding="utf-8")
|
|
100
|
+
failure = subprocess.CalledProcessError(
|
|
101
|
+
returncode=1, cmd=["gh"], stderr="auth failure"
|
|
102
|
+
)
|
|
103
|
+
with patch("subprocess.run", side_effect=failure):
|
|
104
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
105
|
+
reply_to_inline_comment_module.reply_to_inline_comment(
|
|
106
|
+
owner="acme",
|
|
107
|
+
repo="widget",
|
|
108
|
+
number=42,
|
|
109
|
+
comment_id=12345,
|
|
110
|
+
body_file=body_file,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_should_build_body_field_value_from_named_prefix_constant(
|
|
115
|
+
tmp_path: Path,
|
|
116
|
+
) -> None:
|
|
117
|
+
body_file = tmp_path / "reply.md"
|
|
118
|
+
body_file.write_text("Reply text\n", encoding="utf-8")
|
|
119
|
+
with patch("subprocess.run") as mock_run:
|
|
120
|
+
mock_run.return_value = _completed(json.dumps({"id": 7777}))
|
|
121
|
+
reply_to_inline_comment_module.reply_to_inline_comment(
|
|
122
|
+
owner="acme",
|
|
123
|
+
repo="widget",
|
|
124
|
+
number=42,
|
|
125
|
+
comment_id=12345,
|
|
126
|
+
body_file=body_file,
|
|
127
|
+
)
|
|
128
|
+
invoked_argv = mock_run.call_args[0][0]
|
|
129
|
+
field_value = invoked_argv[invoked_argv.index("-F") + 1]
|
|
130
|
+
expected_prefix = (
|
|
131
|
+
reply_to_inline_comment_module.GH_FIELD_BODY_AT_PREFIX
|
|
132
|
+
)
|
|
133
|
+
assert expected_prefix == "body=@"
|
|
134
|
+
assert field_value == f"{expected_prefix}{body_file}"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_should_extract_int_id_from_mixed_typed_response_payload(
|
|
138
|
+
tmp_path: Path,
|
|
139
|
+
) -> None:
|
|
140
|
+
body_file = tmp_path / "reply.md"
|
|
141
|
+
body_file.write_text("Reply text\n", encoding="utf-8")
|
|
142
|
+
response_with_string_and_object_fields = json.dumps(
|
|
143
|
+
{
|
|
144
|
+
"id": 7777,
|
|
145
|
+
"body": "Reply body text",
|
|
146
|
+
"user": {"login": "octocat", "id": 1},
|
|
147
|
+
"created_at": "2026-05-02T12:00:00Z",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
with patch("subprocess.run") as mock_run:
|
|
151
|
+
mock_run.return_value = _completed(response_with_string_and_object_fields)
|
|
152
|
+
reply_id = reply_to_inline_comment_module.reply_to_inline_comment(
|
|
153
|
+
owner="acme",
|
|
154
|
+
repo="widget",
|
|
155
|
+
number=42,
|
|
156
|
+
comment_id=12345,
|
|
157
|
+
body_file=body_file,
|
|
158
|
+
)
|
|
159
|
+
assert reply_id == 7777
|