claude-dev-env 1.35.0 → 1.36.1

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.
Files changed (115) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/bin/install.mjs +28 -8
  3. package/bin/install.test.mjs +9 -1
  4. package/docs/CODE_RULES.md +3 -0
  5. package/docs/agents-md-alignment-plan.md +123 -0
  6. package/hooks/blocking/code_rules_enforcer.py +451 -39
  7. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  8. package/hooks/blocking/test_code_rules_enforcer.py +182 -0
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  10. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  11. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
  12. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
  13. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  14. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
  16. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  17. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  18. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  19. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  20. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  21. package/hooks/config/banned_identifiers_constants.py +24 -0
  22. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  23. package/hooks/config/hook_log_extractor_constants.py +1 -1
  24. package/hooks/config/pre_tool_use_stdin.py +48 -0
  25. package/hooks/config/setup_project_paths_constants.py +4 -0
  26. package/hooks/config/stuttering_check_config.py +14 -0
  27. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  28. package/hooks/config/sys_path_insert_constants.py +4 -0
  29. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  30. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  31. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  32. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  33. package/hooks/config/unused_module_import_constants.py +7 -0
  34. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  35. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  36. package/hooks/git-hooks/config.py +3 -3
  37. package/hooks/git-hooks/test_gate_utils.py +10 -10
  38. package/hooks/mypy.ini +2 -0
  39. package/package.json +1 -1
  40. package/rules/gh-paginate.md +125 -0
  41. package/skills/bugteam/CONSTRAINTS.md +12 -6
  42. package/skills/bugteam/SKILL.md +364 -154
  43. package/skills/bugteam/SKILL_EVALS.md +25 -23
  44. package/skills/bugteam/reference/README.md +2 -0
  45. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  46. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  47. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  48. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  49. package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
  50. package/skills/bugteam/test_skill_additions.py +13 -4
  51. package/skills/bugteam/test_team_lifecycle.py +103 -0
  52. package/skills/findbugs/SKILL.md +3 -3
  53. package/skills/fixbugs/SKILL.md +4 -4
  54. package/skills/monitor-open-prs/SKILL.md +32 -2
  55. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  56. package/skills/pr-converge/SKILL.md +1206 -131
  57. package/skills/pr-converge/scripts/README.md +145 -0
  58. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  59. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  60. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  61. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  62. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  63. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  64. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  65. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  66. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  67. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  68. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  69. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  70. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  71. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  72. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  73. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  74. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  75. package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
  76. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  77. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  78. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  79. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  80. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  81. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  82. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  83. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  84. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  85. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  86. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  87. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  88. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  89. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  90. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  91. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  92. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  93. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  94. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  95. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  96. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  97. package/skills/pr-converge/test_team_lifecycle.py +56 -0
  98. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  99. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  100. package/skills/qbug/SKILL.md +4 -4
  101. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  102. package/skills/resume-review/SKILL.md +261 -0
  103. package/skills/bugteam/scripts/README.md +0 -58
  104. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  105. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  106. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  107. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  108. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  109. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  110. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  111. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  112. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  113. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  114. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  115. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -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
@@ -0,0 +1,101 @@
1
+ """Tests for request_copilot_review.
2
+
3
+ Covers:
4
+ - gh api -X POST is invoked against requested_reviewers endpoint
5
+ - the reviewer id literal is `copilot-pull-request-reviewer[bot]` (the [bot]
6
+ suffix is load-bearing per skills/copilot-review/SKILL.md)
7
+ - subprocess errors propagate
8
+ - the field carries the documented `reviewers[]=<id>` form
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
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 / "request_copilot_review.py"
24
+ spec = importlib.util.spec_from_file_location("request_copilot_review", module_path)
25
+ assert spec is not None
26
+ assert spec.loader is not None
27
+ module = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(module)
29
+ return module
30
+
31
+
32
+ request_copilot_review_module = _load_module()
33
+
34
+
35
+ def _completed(stdout: str) -> subprocess.CompletedProcess:
36
+ process = MagicMock(spec=subprocess.CompletedProcess)
37
+ process.stdout = stdout
38
+ process.returncode = 0
39
+ return process
40
+
41
+
42
+ def test_should_invoke_gh_api_post_against_requested_reviewers_endpoint() -> None:
43
+ with patch("subprocess.run") as mock_run:
44
+ mock_run.return_value = _completed("{}")
45
+ request_copilot_review_module.request_copilot_review(
46
+ owner="acme", repo="widget", number=42
47
+ )
48
+ invoked_argv = mock_run.call_args[0][0]
49
+ assert invoked_argv[0:2] == ["gh", "api"]
50
+ assert "-X" in invoked_argv
51
+ assert "POST" in invoked_argv
52
+ assert "repos/acme/widget/pulls/42/requested_reviewers" in invoked_argv
53
+
54
+
55
+ def test_should_request_copilot_with_bot_suffix_literal() -> None:
56
+ with patch("subprocess.run") as mock_run:
57
+ mock_run.return_value = _completed("{}")
58
+ request_copilot_review_module.request_copilot_review(
59
+ owner="acme", repo="widget", number=42
60
+ )
61
+ invoked_argv = mock_run.call_args[0][0]
62
+ assert "reviewers[]=copilot-pull-request-reviewer[bot]" in invoked_argv
63
+
64
+
65
+ def test_should_pass_reviewer_field_via_dash_f_flag() -> None:
66
+ with patch("subprocess.run") as mock_run:
67
+ mock_run.return_value = _completed("{}")
68
+ request_copilot_review_module.request_copilot_review(
69
+ owner="acme", repo="widget", number=42
70
+ )
71
+ invoked_argv = mock_run.call_args[0][0]
72
+ assert "-f" in invoked_argv
73
+ field_index = invoked_argv.index("-f")
74
+ field_value = invoked_argv[field_index + 1]
75
+ assert field_value == "reviewers[]=copilot-pull-request-reviewer[bot]"
76
+
77
+
78
+ def test_should_raise_when_gh_subprocess_fails() -> None:
79
+ failure = subprocess.CalledProcessError(
80
+ returncode=1, cmd=["gh"], stderr="auth failure"
81
+ )
82
+ with patch("subprocess.run", side_effect=failure):
83
+ with pytest.raises(subprocess.CalledProcessError):
84
+ request_copilot_review_module.request_copilot_review(
85
+ owner="acme", repo="widget", number=42
86
+ )
87
+
88
+
89
+ def test_should_render_endpoint_via_named_template_constant() -> None:
90
+ with patch("subprocess.run") as mock_run:
91
+ mock_run.return_value = _completed("{}")
92
+ request_copilot_review_module.request_copilot_review(
93
+ owner="acme", repo="widget", number=42
94
+ )
95
+ invoked_argv = mock_run.call_args[0][0]
96
+ expected_endpoint = (
97
+ request_copilot_review_module.GH_REQUESTED_REVIEWERS_PATH_TEMPLATE.format(
98
+ owner="acme", repo="widget", number=42
99
+ )
100
+ )
101
+ assert expected_endpoint in invoked_argv
@@ -0,0 +1,79 @@
1
+ """Tests for resolve_pr_head.
2
+
3
+ Covers:
4
+ - gh command targets the single-object PR endpoint with --jq .head.sha
5
+ - single-object endpoint does NOT use --paginate or --slurp (per gh-paginate rule)
6
+ - the trimmed SHA is returned
7
+ - subprocess errors propagate
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ import subprocess
14
+ from pathlib import Path
15
+ from types import ModuleType
16
+ from unittest.mock import MagicMock, patch
17
+
18
+ import pytest
19
+
20
+
21
+ def _load_module() -> ModuleType:
22
+ module_path = Path(__file__).parent / "resolve_pr_head.py"
23
+ spec = importlib.util.spec_from_file_location("resolve_pr_head", module_path)
24
+ assert spec is not None
25
+ assert spec.loader is not None
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ resolve_pr_head_module = _load_module()
32
+
33
+
34
+ def _completed(stdout: str) -> subprocess.CompletedProcess:
35
+ process = MagicMock(spec=subprocess.CompletedProcess)
36
+ process.stdout = stdout
37
+ process.returncode = 0
38
+ return process
39
+
40
+
41
+ def test_should_invoke_gh_against_single_object_pr_endpoint() -> None:
42
+ with patch("subprocess.run") as mock_run:
43
+ mock_run.return_value = _completed("85309a0789e\n")
44
+ resolve_pr_head_module.resolve_pr_head(owner="acme", repo="widget", number=42)
45
+ invoked_argv = mock_run.call_args[0][0]
46
+ assert invoked_argv[0] == "gh"
47
+ assert invoked_argv[1] == "api"
48
+ assert invoked_argv[2] == "repos/acme/widget/pulls/42"
49
+ assert "--jq" in invoked_argv
50
+ assert ".head.sha" in invoked_argv
51
+
52
+
53
+ def test_should_not_use_paginate_or_slurp_on_single_object_endpoint() -> None:
54
+ with patch("subprocess.run") as mock_run:
55
+ mock_run.return_value = _completed("abc\n")
56
+ resolve_pr_head_module.resolve_pr_head(owner="acme", repo="widget", number=42)
57
+ invoked_argv = mock_run.call_args[0][0]
58
+ assert "--paginate" not in invoked_argv
59
+ assert "--slurp" not in invoked_argv
60
+
61
+
62
+ def test_should_return_trimmed_sha() -> None:
63
+ with patch("subprocess.run") as mock_run:
64
+ mock_run.return_value = _completed(" 85309a0789e \n")
65
+ resolved_sha = resolve_pr_head_module.resolve_pr_head(
66
+ owner="acme", repo="widget", number=42
67
+ )
68
+ assert resolved_sha == "85309a0789e"
69
+
70
+
71
+ def test_should_raise_when_gh_subprocess_fails() -> None:
72
+ failure = subprocess.CalledProcessError(
73
+ returncode=1, cmd=["gh"], stderr="auth failure"
74
+ )
75
+ with patch("subprocess.run", side_effect=failure):
76
+ with pytest.raises(subprocess.CalledProcessError):
77
+ resolve_pr_head_module.resolve_pr_head(
78
+ owner="acme", repo="widget", number=42
79
+ )
@@ -0,0 +1,80 @@
1
+ """Tests for review_field_helpers.
2
+
3
+ Covers defensive field-coercion for the four GitHub payload field accessors
4
+ shared across fetch_*_reviews.py and fetch_*_inline_comments.py.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib.util
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+
13
+
14
+ def _load_module() -> ModuleType:
15
+ module_path = Path(__file__).parent / "review_field_helpers.py"
16
+ spec = importlib.util.spec_from_file_location("review_field_helpers", module_path)
17
+ assert spec is not None
18
+ assert spec.loader is not None
19
+ module = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(module)
21
+ return module
22
+
23
+
24
+ review_field_helpers_module = _load_module()
25
+
26
+
27
+ def test_login_of_should_return_login_string_when_user_dict_has_login() -> None:
28
+ payload = {"user": {"login": "cursor[bot]"}}
29
+ assert review_field_helpers_module.login_of(payload) == "cursor[bot]"
30
+
31
+
32
+ def test_login_of_should_return_none_when_user_field_missing() -> None:
33
+ assert review_field_helpers_module.login_of({}) is None
34
+
35
+
36
+ def test_login_of_should_return_none_when_user_field_not_dict() -> None:
37
+ assert review_field_helpers_module.login_of({"user": "not-a-dict"}) is None
38
+
39
+
40
+ def test_login_of_should_return_none_when_login_field_not_string() -> None:
41
+ assert review_field_helpers_module.login_of({"user": {"login": 12345}}) is None
42
+
43
+
44
+ def test_body_of_should_return_body_string_when_present() -> None:
45
+ assert review_field_helpers_module.body_of({"body": "review body"}) == "review body"
46
+
47
+
48
+ def test_body_of_should_return_empty_string_when_body_missing() -> None:
49
+ assert review_field_helpers_module.body_of({}) == ""
50
+
51
+
52
+ def test_body_of_should_return_empty_string_when_body_not_string() -> None:
53
+ assert review_field_helpers_module.body_of({"body": None}) == ""
54
+
55
+
56
+ def test_submitted_at_of_should_return_string_when_present() -> None:
57
+ payload = {"submitted_at": "2026-05-03T12:00:00Z"}
58
+ assert (
59
+ review_field_helpers_module.submitted_at_of(payload) == "2026-05-03T12:00:00Z"
60
+ )
61
+
62
+
63
+ def test_submitted_at_of_should_return_empty_string_when_missing() -> None:
64
+ assert review_field_helpers_module.submitted_at_of({}) == ""
65
+
66
+
67
+ def test_submitted_at_of_should_return_empty_string_when_not_string() -> None:
68
+ assert review_field_helpers_module.submitted_at_of({"submitted_at": 0}) == ""
69
+
70
+
71
+ def test_state_of_should_return_state_string_when_present() -> None:
72
+ assert review_field_helpers_module.state_of({"state": "APPROVED"}) == "APPROVED"
73
+
74
+
75
+ def test_state_of_should_return_empty_string_when_missing() -> None:
76
+ assert review_field_helpers_module.state_of({}) == ""
77
+
78
+
79
+ def test_state_of_should_return_empty_string_when_not_string() -> None:
80
+ assert review_field_helpers_module.state_of({"state": ["APPROVED"]}) == ""