claude-dev-env 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/sr-loop.md +9 -5
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
"""Behavior tests for the proof-of-work comment audit and gh pr ready gate.
|
|
2
|
-
|
|
3
|
-
Each test drives the public audit surface of pr_description_proof_of_work
|
|
4
|
-
with real comment bodies. The single gh subprocess boundary is patched with
|
|
5
|
-
a fake runner, so parsing, part detection, and gate decisions all run the
|
|
6
|
-
production code paths.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import pathlib
|
|
11
|
-
import sys
|
|
12
|
-
|
|
13
|
-
import pytest
|
|
14
|
-
|
|
15
|
-
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
16
|
-
_HOOKS_ROOT = _HOOK_DIR.parent
|
|
17
|
-
if str(_HOOKS_ROOT) not in sys.path:
|
|
18
|
-
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
19
|
-
|
|
20
|
-
from blocking import convergence_gate_blocker as convergence_module # noqa: E402
|
|
21
|
-
from blocking import pr_description_proof_of_work as proof_module # noqa: E402
|
|
22
|
-
from blocking.pr_description_proof_of_work import ( # noqa: E402
|
|
23
|
-
audit_proof_comment_body,
|
|
24
|
-
evaluate_pr_ready_gate,
|
|
25
|
-
is_pr_ready_command,
|
|
26
|
-
is_proof_shaped_body,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
PASSING_PROOF_COMMENT_BODY = (
|
|
30
|
-
"## Summary\n\n"
|
|
31
|
-
"Recolored the launcher icons and checked every produced asset by hand "
|
|
32
|
-
"against the palette the plan names.\n\n"
|
|
33
|
-
"## Verification\n\n"
|
|
34
|
-
"```\n"
|
|
35
|
-
"C:\\Python313\\python.exe scripts/recolor.py --input themes/dawn\n"
|
|
36
|
-
"```\n\n"
|
|
37
|
-
"| Artifact | Measured |\n"
|
|
38
|
-
"| --- | --- |\n"
|
|
39
|
-
"| icons.zip | 48 files, 412,993 bytes |\n\n"
|
|
40
|
-
"This advances phase 2 of issue #12.\n\n"
|
|
41
|
-
"Known gap: the offline proof cannot show the on-device rendering; the "
|
|
42
|
-
"store preview covers that.\n"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
VISUAL_PROOF_COMMENT_BODY = (
|
|
46
|
-
PASSING_PROOF_COMMENT_BODY + "\n\n"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
BARE_PROOF_COMMENT_BODY = "## Verification\n\nLooks good overall to me.\n"
|
|
50
|
-
|
|
51
|
-
ORDINARY_COMMENT_BODY = (
|
|
52
|
-
"The retry logic here mirrors what the uploader does for chunked "
|
|
53
|
-
"transfers, so the two stay in step."
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _install_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text):
|
|
58
|
-
all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
|
|
59
|
-
|
|
60
|
-
def _fake_run_gh_command(all_gh_arguments):
|
|
61
|
-
if all_gh_arguments and all_gh_arguments[0] == "api":
|
|
62
|
-
return json.dumps([all_comment_records])
|
|
63
|
-
if list(all_gh_arguments[:2]) == ["pr", "view"]:
|
|
64
|
-
return json.dumps({"number": 77})
|
|
65
|
-
if "--name-only" in all_gh_arguments:
|
|
66
|
-
return "\n".join(all_diff_names)
|
|
67
|
-
if list(all_gh_arguments[:2]) == ["pr", "diff"]:
|
|
68
|
-
return diff_text
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _install_recording_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text):
|
|
75
|
-
all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
|
|
76
|
-
all_recorded_argument_lists: list[list[str]] = []
|
|
77
|
-
|
|
78
|
-
def _fake_run_gh_command(all_gh_arguments):
|
|
79
|
-
all_recorded_argument_lists.append(list(all_gh_arguments))
|
|
80
|
-
if all_gh_arguments and all_gh_arguments[0] == "api":
|
|
81
|
-
return json.dumps([all_comment_records])
|
|
82
|
-
if list(all_gh_arguments[:2]) == ["pr", "view"]:
|
|
83
|
-
return json.dumps({"number": 77})
|
|
84
|
-
if "--name-only" in all_gh_arguments:
|
|
85
|
-
return "\n".join(all_diff_names)
|
|
86
|
-
if list(all_gh_arguments[:2]) == ["pr", "diff"]:
|
|
87
|
-
return diff_text
|
|
88
|
-
return None
|
|
89
|
-
|
|
90
|
-
monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
|
|
91
|
-
return all_recorded_argument_lists
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _argument_lists_with_leading(all_recorded_argument_lists, first_token, second_token):
|
|
95
|
-
return [
|
|
96
|
-
each_arguments
|
|
97
|
-
for each_arguments in all_recorded_argument_lists
|
|
98
|
-
if list(each_arguments[:2]) == [first_token, second_token]
|
|
99
|
-
]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_is_proof_shaped_body_detects_verification_heading() -> None:
|
|
103
|
-
assert is_proof_shaped_body(PASSING_PROOF_COMMENT_BODY)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_is_proof_shaped_body_skips_ordinary_comment() -> None:
|
|
107
|
-
assert not is_proof_shaped_body(ORDINARY_COMMENT_BODY)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_is_pr_ready_command_matches_ready() -> None:
|
|
111
|
-
assert is_pr_ready_command("gh pr ready 123")
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_is_pr_ready_command_skips_undo() -> None:
|
|
115
|
-
assert not is_pr_ready_command("gh pr ready 123 --undo")
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_is_pr_ready_command_matches_after_shell_operator() -> None:
|
|
119
|
-
assert is_pr_ready_command("git fetch && gh pr ready 5")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def test_is_pr_ready_command_ignores_phrase_inside_commit_message() -> None:
|
|
123
|
-
assert not is_pr_ready_command(
|
|
124
|
-
'git commit -m "block gh pr ready without a proof comment"'
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_is_pr_ready_command_ignores_phrase_inside_search_argument() -> None:
|
|
129
|
-
assert not is_pr_ready_command('grep -r "gh pr ready" packages/')
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_audit_passes_complete_proof_on_non_visual_change(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
133
|
-
_install_fake_gh(monkeypatch, [], ["src/parser.py"], "+ plain code line")
|
|
134
|
-
assert audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123) == []
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def test_audit_flags_missing_visual_on_image_diff(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
138
|
-
_install_fake_gh(monkeypatch, [], ["assets/icon.png"], "")
|
|
139
|
-
all_missing_parts = audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123)
|
|
140
|
-
assert any("visual" in each_part for each_part in all_missing_parts)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_audit_accepts_image_embed_on_image_diff(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
144
|
-
_install_fake_gh(monkeypatch, [], ["assets/icon.png"], "")
|
|
145
|
-
assert audit_proof_comment_body(VISUAL_PROOF_COMMENT_BODY, 123) == []
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def test_audit_detects_visual_change_from_hex_colors(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
149
|
-
_install_fake_gh(monkeypatch, [], ["src/theme.py"], '+ accent = "#AABBCC"')
|
|
150
|
-
all_missing_parts = audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123)
|
|
151
|
-
assert any("visual" in each_part for each_part in all_missing_parts)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_audit_names_every_missing_part_of_bare_proof(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
155
|
-
_install_fake_gh(monkeypatch, [], ["src/parser.py"], "")
|
|
156
|
-
all_missing_parts = audit_proof_comment_body(BARE_PROOF_COMMENT_BODY, 123)
|
|
157
|
-
assert len(all_missing_parts) == 4
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def test_ready_gate_blocks_when_no_proof_comment_exists(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
161
|
-
_install_fake_gh(monkeypatch, [ORDINARY_COMMENT_BODY], ["src/parser.py"], "")
|
|
162
|
-
denial_reason = evaluate_pr_ready_gate("gh pr ready 123")
|
|
163
|
-
assert denial_reason is not None
|
|
164
|
-
assert "proof" in denial_reason.lower()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_ready_gate_allows_with_passing_proof_comment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
168
|
-
_install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
169
|
-
assert evaluate_pr_ready_gate("gh pr ready 123") is None
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def test_ready_gate_blocks_when_proof_comment_is_incomplete(
|
|
173
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
174
|
-
) -> None:
|
|
175
|
-
_install_fake_gh(monkeypatch, [BARE_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
176
|
-
denial_reason = evaluate_pr_ready_gate("gh pr ready 123")
|
|
177
|
-
assert denial_reason is not None
|
|
178
|
-
assert "123" in denial_reason
|
|
179
|
-
assert "proof" in denial_reason.lower()
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def test_ready_gate_fails_open_when_gh_is_unavailable(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
183
|
-
monkeypatch.setattr(proof_module, "_run_gh_command", lambda all_gh_arguments: None)
|
|
184
|
-
assert evaluate_pr_ready_gate("gh pr ready 123") is None
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def test_ready_gate_resolves_pr_number_from_current_branch(
|
|
188
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
189
|
-
) -> None:
|
|
190
|
-
_install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
191
|
-
monkeypatch.setattr(
|
|
192
|
-
convergence_module,
|
|
193
|
-
"_resolve_current_branch_pr_number",
|
|
194
|
-
lambda all_target_repo, cwd: 77,
|
|
195
|
-
)
|
|
196
|
-
assert evaluate_pr_ready_gate("gh pr ready") is None
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def test_ready_gate_reads_comments_from_repo_named_by_flag(
|
|
200
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
201
|
-
) -> None:
|
|
202
|
-
all_recorded = _install_recording_fake_gh(
|
|
203
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
204
|
-
)
|
|
205
|
-
evaluate_pr_ready_gate("gh pr ready 42 --repo other-owner/other-repo")
|
|
206
|
-
all_api_calls = _argument_lists_with_leading(
|
|
207
|
-
all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
|
|
208
|
-
)
|
|
209
|
-
assert all_api_calls
|
|
210
|
-
for each_arguments in all_api_calls:
|
|
211
|
-
assert "{owner}" not in each_arguments[1]
|
|
212
|
-
assert "{repo}" not in each_arguments[1]
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def test_ready_gate_resolves_pr_number_from_repo_named_by_flag(
|
|
216
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
217
|
-
) -> None:
|
|
218
|
-
_install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
219
|
-
all_captured_repo_pairs: list[tuple[str, str] | None] = []
|
|
220
|
-
|
|
221
|
-
def fake_resolver(all_target_repo: tuple[str, str] | None, cwd: str | None) -> int:
|
|
222
|
-
all_captured_repo_pairs.append(all_target_repo)
|
|
223
|
-
return 77
|
|
224
|
-
|
|
225
|
-
monkeypatch.setattr(convergence_module, "_resolve_current_branch_pr_number", fake_resolver)
|
|
226
|
-
evaluate_pr_ready_gate("gh pr ready --repo other-owner/other-repo")
|
|
227
|
-
assert all_captured_repo_pairs == [("other-owner", "other-repo")]
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def test_ready_gate_reads_comments_from_repo_named_by_url(
|
|
231
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
232
|
-
) -> None:
|
|
233
|
-
all_recorded = _install_recording_fake_gh(
|
|
234
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
235
|
-
)
|
|
236
|
-
evaluate_pr_ready_gate("gh pr ready https://github.com/url-owner/url-repo/pull/99")
|
|
237
|
-
all_api_calls = _argument_lists_with_leading(
|
|
238
|
-
all_recorded, "api", "repos/url-owner/url-repo/issues/99/comments"
|
|
239
|
-
)
|
|
240
|
-
assert all_api_calls
|
|
241
|
-
all_view_calls = _argument_lists_with_leading(all_recorded, "pr", "view")
|
|
242
|
-
assert not all_view_calls
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def test_ready_gate_leaves_bare_command_bound_to_current_directory(
|
|
246
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
247
|
-
) -> None:
|
|
248
|
-
all_recorded = _install_recording_fake_gh(
|
|
249
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
250
|
-
)
|
|
251
|
-
evaluate_pr_ready_gate("gh pr ready 123")
|
|
252
|
-
all_api_calls = _argument_lists_with_leading(
|
|
253
|
-
all_recorded, "api", "repos/{owner}/{repo}/issues/123/comments"
|
|
254
|
-
)
|
|
255
|
-
assert all_api_calls
|
|
256
|
-
for each_arguments in all_recorded:
|
|
257
|
-
assert "--repo" not in each_arguments
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def test_ready_gate_ignores_repo_flag_of_chained_command(
|
|
261
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
262
|
-
) -> None:
|
|
263
|
-
all_recorded = _install_recording_fake_gh(
|
|
264
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
265
|
-
)
|
|
266
|
-
evaluate_pr_ready_gate(
|
|
267
|
-
"gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo"
|
|
268
|
-
)
|
|
269
|
-
all_api_calls = _argument_lists_with_leading(
|
|
270
|
-
all_recorded, "api", "repos/{owner}/{repo}/issues/161/comments"
|
|
271
|
-
)
|
|
272
|
-
assert all_api_calls
|
|
273
|
-
for each_arguments in all_recorded:
|
|
274
|
-
assert "other-owner/other-repo" not in each_arguments
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def test_ready_gate_reads_comments_from_repo_named_by_full_url_flag(
|
|
278
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
279
|
-
) -> None:
|
|
280
|
-
all_recorded = _install_recording_fake_gh(
|
|
281
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
282
|
-
)
|
|
283
|
-
evaluate_pr_ready_gate("gh pr ready 42 --repo https://github.com/other-owner/other-repo")
|
|
284
|
-
all_api_calls = _argument_lists_with_leading(
|
|
285
|
-
all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
|
|
286
|
-
)
|
|
287
|
-
assert all_api_calls
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def test_ready_gate_reads_comments_from_repo_named_by_short_url_flag(
|
|
291
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
292
|
-
) -> None:
|
|
293
|
-
all_recorded = _install_recording_fake_gh(
|
|
294
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
295
|
-
)
|
|
296
|
-
evaluate_pr_ready_gate("gh pr ready 42 -R https://github.com/other-owner/other-repo")
|
|
297
|
-
all_api_calls = _argument_lists_with_leading(
|
|
298
|
-
all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
|
|
299
|
-
)
|
|
300
|
-
assert all_api_calls
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def test_ready_gate_reads_comments_from_repo_named_by_ssh_flag(
|
|
304
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
305
|
-
) -> None:
|
|
306
|
-
all_recorded = _install_recording_fake_gh(
|
|
307
|
-
monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
|
|
308
|
-
)
|
|
309
|
-
evaluate_pr_ready_gate("gh pr ready 42 --repo git@github.com:other-owner/other-repo")
|
|
310
|
-
all_api_calls = _argument_lists_with_leading(
|
|
311
|
-
all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
|
|
312
|
-
)
|
|
313
|
-
assert all_api_calls
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
"""Unit tests for reviewer_spawn_gate PreToolUse hook."""
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
import io
|
|
5
|
-
import json
|
|
6
|
-
import pathlib
|
|
7
|
-
import sys
|
|
8
|
-
from contextlib import redirect_stderr, redirect_stdout
|
|
9
|
-
|
|
10
|
-
import pytest
|
|
11
|
-
|
|
12
|
-
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
13
|
-
if str(_HOOK_DIR) not in sys.path:
|
|
14
|
-
sys.path.insert(0, str(_HOOK_DIR))
|
|
15
|
-
|
|
16
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
17
|
-
"reviewer_spawn_gate",
|
|
18
|
-
_HOOK_DIR / "reviewer_spawn_gate.py",
|
|
19
|
-
)
|
|
20
|
-
assert hook_spec is not None
|
|
21
|
-
assert hook_spec.loader is not None
|
|
22
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
23
|
-
hook_spec.loader.exec_module(hook_module)
|
|
24
|
-
|
|
25
|
-
_matches_copilot_trigger = hook_module._matches_copilot_trigger
|
|
26
|
-
_matches_bugbot_trigger = hook_module._matches_bugbot_trigger
|
|
27
|
-
_reviewer_trigger = hook_module._reviewer_trigger
|
|
28
|
-
|
|
29
|
-
_SENTINEL_MARKER = hook_module.GATE_SENTINEL_MARKER
|
|
30
|
-
_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME = hook_module.AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME
|
|
31
|
-
|
|
32
|
-
_COPILOT_TRIGGER_COMMAND = (
|
|
33
|
-
f"{_SENTINEL_MARKER} gh api repos/o/r/pulls/1/requested_reviewers "
|
|
34
|
-
f'-f "reviewers[]=copilot-pull-request-reviewer[bot]"'
|
|
35
|
-
)
|
|
36
|
-
_BUGBOT_TRIGGER_COMMAND = f'{_SENTINEL_MARKER} python post_fix_reply.py --body "bugbot run"'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _write_fake_availability_script(tmp_path: pathlib.Path, exit_code: int) -> pathlib.Path:
|
|
40
|
-
fake_script_path = tmp_path / "fake_reviewer_availability.py"
|
|
41
|
-
fake_script_path.write_text(f"import sys\nsys.exit({exit_code})\n", encoding="utf-8")
|
|
42
|
-
return fake_script_path
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _run_hook_with_stdin_text(stdin_text: str) -> tuple[str, str, int]:
|
|
46
|
-
captured_stdout = io.StringIO()
|
|
47
|
-
captured_stderr = io.StringIO()
|
|
48
|
-
exit_code = 0
|
|
49
|
-
sys.stdin = io.StringIO(stdin_text)
|
|
50
|
-
try:
|
|
51
|
-
with redirect_stdout(captured_stdout), redirect_stderr(captured_stderr):
|
|
52
|
-
try:
|
|
53
|
-
hook_module.main()
|
|
54
|
-
except SystemExit as exit_signal:
|
|
55
|
-
exit_code = exit_signal.code if isinstance(exit_signal.code, int) else 0
|
|
56
|
-
finally:
|
|
57
|
-
sys.stdin = sys.__stdin__
|
|
58
|
-
return captured_stdout.getvalue(), captured_stderr.getvalue(), exit_code
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _run_hook(hook_input: dict) -> tuple[str, int]:
|
|
62
|
-
stdout_text, _stderr_text, exit_code = _run_hook_with_stdin_text(json.dumps(hook_input))
|
|
63
|
-
return stdout_text, exit_code
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_matches_copilot_trigger_true_for_request_reviewers_command() -> None:
|
|
67
|
-
assert _matches_copilot_trigger(_COPILOT_TRIGGER_COMMAND)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_matches_copilot_trigger_false_for_unrelated_command() -> None:
|
|
71
|
-
assert not _matches_copilot_trigger("gh pr list")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_matches_bugbot_trigger_true_for_rerun_comment_command() -> None:
|
|
75
|
-
assert _matches_bugbot_trigger(_BUGBOT_TRIGGER_COMMAND)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def test_matches_bugbot_trigger_false_for_unrelated_command() -> None:
|
|
79
|
-
assert not _matches_bugbot_trigger("python post_fix_reply.py --body 'looks good'")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def test_reviewer_trigger_returns_none_for_unrecognized_command() -> None:
|
|
83
|
-
assert _reviewer_trigger("gh pr list") is None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def test_main_allows_copilot_shaped_command_with_no_sentinel_marker(
|
|
87
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
88
|
-
) -> None:
|
|
89
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
90
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
91
|
-
stdout_text, exit_code = _run_hook(
|
|
92
|
-
{
|
|
93
|
-
"tool_name": "Bash",
|
|
94
|
-
"tool_input": {
|
|
95
|
-
"command": (
|
|
96
|
-
"gh api repos/o/r/pulls/1/requested_reviewers "
|
|
97
|
-
'-f "reviewers[]=copilot-pull-request-reviewer[bot]"'
|
|
98
|
-
)
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
assert exit_code == 0
|
|
103
|
-
assert stdout_text == ""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_main_allows_copilot_trigger_when_available(
|
|
107
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
108
|
-
) -> None:
|
|
109
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=0)
|
|
110
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
111
|
-
stdout_text, exit_code = _run_hook(
|
|
112
|
-
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
113
|
-
)
|
|
114
|
-
assert exit_code == 0
|
|
115
|
-
assert stdout_text == ""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_main_denies_copilot_trigger_when_down(
|
|
119
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
120
|
-
) -> None:
|
|
121
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
122
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
123
|
-
stdout_text, exit_code = _run_hook(
|
|
124
|
-
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
125
|
-
)
|
|
126
|
-
assert exit_code == 0
|
|
127
|
-
response_payload = json.loads(stdout_text)
|
|
128
|
-
decision_block = response_payload["hookSpecificOutput"]
|
|
129
|
-
assert decision_block["permissionDecision"] == "deny"
|
|
130
|
-
assert "GitHub Copilot" in decision_block["permissionDecisionReason"]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_main_allows_copilot_trigger_on_unexpected_exit_code(
|
|
134
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
135
|
-
) -> None:
|
|
136
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=2)
|
|
137
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
138
|
-
stdout_text, exit_code = _run_hook(
|
|
139
|
-
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
140
|
-
)
|
|
141
|
-
assert exit_code == 0
|
|
142
|
-
assert stdout_text == ""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_main_allows_copilot_trigger_when_availability_script_crashes(
|
|
146
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
147
|
-
) -> None:
|
|
148
|
-
fake_script_path = tmp_path / "fake_reviewer_availability.py"
|
|
149
|
-
fake_script_path.write_text(
|
|
150
|
-
"import module_that_does_not_exist_anywhere\n", encoding="utf-8"
|
|
151
|
-
)
|
|
152
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
153
|
-
stdout_text, exit_code = _run_hook(
|
|
154
|
-
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
155
|
-
)
|
|
156
|
-
assert exit_code == 0
|
|
157
|
-
assert stdout_text == ""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def test_main_allows_bugbot_trigger_when_available(
|
|
161
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
162
|
-
) -> None:
|
|
163
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=0)
|
|
164
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
165
|
-
stdout_text, exit_code = _run_hook(
|
|
166
|
-
{"tool_name": "Bash", "tool_input": {"command": _BUGBOT_TRIGGER_COMMAND}}
|
|
167
|
-
)
|
|
168
|
-
assert exit_code == 0
|
|
169
|
-
assert stdout_text == ""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def test_main_denies_bugbot_trigger_when_opted_out(
|
|
173
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
174
|
-
) -> None:
|
|
175
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
176
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
177
|
-
stdout_text, exit_code = _run_hook(
|
|
178
|
-
{"tool_name": "Bash", "tool_input": {"command": _BUGBOT_TRIGGER_COMMAND}}
|
|
179
|
-
)
|
|
180
|
-
assert exit_code == 0
|
|
181
|
-
response_payload = json.loads(stdout_text)
|
|
182
|
-
decision_block = response_payload["hookSpecificOutput"]
|
|
183
|
-
assert decision_block["permissionDecision"] == "deny"
|
|
184
|
-
assert "Cursor Bugbot" in decision_block["permissionDecisionReason"]
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
def test_main_allows_marker_command_with_no_recognized_trigger(
|
|
188
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
189
|
-
) -> None:
|
|
190
|
-
fake_script_path = _write_fake_availability_script(tmp_path, exit_code=3)
|
|
191
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(fake_script_path))
|
|
192
|
-
stdout_text, exit_code = _run_hook(
|
|
193
|
-
{"tool_name": "Bash", "tool_input": {"command": f"{_SENTINEL_MARKER} gh pr list"}}
|
|
194
|
-
)
|
|
195
|
-
assert exit_code == 0
|
|
196
|
-
assert stdout_text == ""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def test_main_allows_when_availability_script_missing(
|
|
200
|
-
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
201
|
-
) -> None:
|
|
202
|
-
missing_script_path = tmp_path / "does_not_exist.py"
|
|
203
|
-
monkeypatch.setenv(_AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, str(missing_script_path))
|
|
204
|
-
stdout_text, exit_code = _run_hook(
|
|
205
|
-
{"tool_name": "Bash", "tool_input": {"command": _COPILOT_TRIGGER_COMMAND}}
|
|
206
|
-
)
|
|
207
|
-
assert exit_code == 0
|
|
208
|
-
assert stdout_text == ""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def test_main_allows_non_bash_tool() -> None:
|
|
212
|
-
stdout_text, exit_code = _run_hook(
|
|
213
|
-
{"tool_name": "Write", "tool_input": {"content": _COPILOT_TRIGGER_COMMAND}}
|
|
214
|
-
)
|
|
215
|
-
assert exit_code == 0
|
|
216
|
-
assert stdout_text == ""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def test_main_with_empty_stdin_exits_silently() -> None:
|
|
220
|
-
stdout_text, stderr_text, exit_code = _run_hook_with_stdin_text("")
|
|
221
|
-
assert exit_code == 0
|
|
222
|
-
assert stdout_text == ""
|
|
223
|
-
assert stderr_text == ""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def test_main_with_invalid_json_stdin_exits_silently() -> None:
|
|
227
|
-
stdout_text, stderr_text, exit_code = _run_hook_with_stdin_text("{broken")
|
|
228
|
-
assert exit_code == 0
|
|
229
|
-
assert stdout_text == ""
|
|
230
|
-
assert stderr_text == ""
|