claude-dev-env 1.89.0 → 1.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +0 -13
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/agents/clean-coder.md +2 -2
- package/agents/code-verifier.md +0 -1
- package/agents/test_agent_frontmatter.py +78 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/bin/install.mjs +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +6 -2
- package/hooks/blocking/code_rules_comments.py +2 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
- package/hooks/blocking/config/verified_commit_constants.py +2 -0
- package/hooks/blocking/conftest.py +115 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
- package/hooks/blocking/pr_description_enforcer.py +46 -22
- package/hooks/blocking/pr_description_pr_number.py +5 -3
- package/hooks/blocking/pr_description_proof_of_work.py +367 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
- package/hooks/blocking/test_verification_verdict_store.py +11 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
- package/hooks/blocking/test_verified_commit_gate.py +11 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
- package/hooks/blocking/verification_verdict_store.py +73 -5
- package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
- package/hooks/blocking/verified_commit_gate.py +6 -0
- package/hooks/blocking/verifier_verdict_minter.py +6 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +32 -2
- package/hooks/hooks_constants/CLAUDE.md +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/hooks/validators/run_all_validators.py +216 -4
- package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +3 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/scripts/CLAUDE.md +2 -0
- package/scripts/claude-chain.example.json +8 -0
- package/scripts/claude_chain_runner.py +400 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/team-advisor/SKILL.md +188 -0
- package/skills/team-advisor-refresh/SKILL.md +25 -0
- package/skills/usage-pause/SKILL.md +108 -0
- package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
- package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
- package/system-prompts/software-engineer.xml +3 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Entry-flow tests for the proof-of-work gates in pr_description_enforcer.
|
|
2
|
+
|
|
3
|
+
Each test drives hook main() with a real Bash PreToolUse payload on stdin,
|
|
4
|
+
mirroring test_pr_description_enforcer.py. The single gh subprocess boundary
|
|
5
|
+
in pr_description_proof_of_work is patched with a fake runner, so command
|
|
6
|
+
recognition, body extraction, and the proof audit run the production paths.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import importlib.util
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import pathlib
|
|
14
|
+
import sys
|
|
15
|
+
from unittest.mock import patch
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
20
|
+
_HOOKS_ROOT = _HOOK_DIR.parent
|
|
21
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
22
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
23
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
25
|
+
|
|
26
|
+
from blocking import pr_description_proof_of_work as proof_module # noqa: E402
|
|
27
|
+
from blocking import pr_description_readability as readability_module # noqa: E402
|
|
28
|
+
|
|
29
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
30
|
+
"pr_description_enforcer_proof_gate",
|
|
31
|
+
_HOOK_DIR / "pr_description_enforcer.py",
|
|
32
|
+
)
|
|
33
|
+
assert hook_spec is not None
|
|
34
|
+
assert hook_spec.loader is not None
|
|
35
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
36
|
+
hook_spec.loader.exec_module(hook_module)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture(autouse=True)
|
|
40
|
+
def _isolate_readability_state(
|
|
41
|
+
tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Redirect the three readability state files to per-test temp paths.
|
|
44
|
+
|
|
45
|
+
The enabled file is written with enabled=False so readability scoring
|
|
46
|
+
stays out of these entry-flow tests and the live state directory is
|
|
47
|
+
never touched.
|
|
48
|
+
"""
|
|
49
|
+
per_test_state_dir = tmp_path_factory.mktemp("proof_gate_readability_state")
|
|
50
|
+
monkeypatch.setattr(
|
|
51
|
+
readability_module, "READABILITY_STATE_FILE", per_test_state_dir / "strikes.json"
|
|
52
|
+
)
|
|
53
|
+
monkeypatch.setattr(
|
|
54
|
+
readability_module,
|
|
55
|
+
"READABILITY_THRESHOLD_OVERRIDE_FILE",
|
|
56
|
+
per_test_state_dir / "overrides.json",
|
|
57
|
+
)
|
|
58
|
+
enabled_path = per_test_state_dir / "enabled.json"
|
|
59
|
+
enabled_path.write_text(json.dumps({"enabled": False}))
|
|
60
|
+
monkeypatch.setattr(readability_module, "READABILITY_ENABLED_STATE_FILE", enabled_path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
PASSING_PROOF_COMMENT_BODY = (
|
|
64
|
+
"## Summary\n\n"
|
|
65
|
+
"Recolored the launcher icons and checked every produced asset by hand "
|
|
66
|
+
"against the palette the plan names.\n\n"
|
|
67
|
+
"## Verification\n\n"
|
|
68
|
+
"```\n"
|
|
69
|
+
"C:\\Python313\\python.exe scripts/recolor.py --input themes/dawn\n"
|
|
70
|
+
"```\n\n"
|
|
71
|
+
"| Artifact | Measured |\n"
|
|
72
|
+
"| --- | --- |\n"
|
|
73
|
+
"| icons.zip | 48 files, 412,993 bytes |\n\n"
|
|
74
|
+
"This advances phase 2 of issue #12.\n\n"
|
|
75
|
+
"Known gap: the offline proof cannot show the on-device rendering; the "
|
|
76
|
+
"store preview covers that.\n"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
ORDINARY_COMMENT_BODY = (
|
|
80
|
+
"The retry logic here mirrors what the uploader does for chunked "
|
|
81
|
+
"transfers, so the two stay in step."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _run_hook_main_with_command(command: str) -> str:
|
|
86
|
+
hook_input_json = json.dumps({"tool_name": "Bash", "tool_input": {"command": command}})
|
|
87
|
+
captured_stdout = io.StringIO()
|
|
88
|
+
with (
|
|
89
|
+
patch("sys.stdin", io.StringIO(hook_input_json)),
|
|
90
|
+
patch("sys.stdout", captured_stdout),
|
|
91
|
+
contextlib.suppress(SystemExit),
|
|
92
|
+
):
|
|
93
|
+
hook_module.main()
|
|
94
|
+
return captured_stdout.getvalue()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _install_fake_gh_responses(monkeypatch, all_comment_bodies, all_diff_names):
|
|
98
|
+
all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
|
|
99
|
+
|
|
100
|
+
def _fake_run_gh_command(all_gh_arguments):
|
|
101
|
+
if all_gh_arguments and all_gh_arguments[0] == "api":
|
|
102
|
+
return json.dumps([all_comment_records])
|
|
103
|
+
if "--name-only" in all_gh_arguments:
|
|
104
|
+
return "\n".join(all_diff_names)
|
|
105
|
+
if list(all_gh_arguments[:2]) == ["pr", "view"]:
|
|
106
|
+
return json.dumps({"number": 123})
|
|
107
|
+
if list(all_gh_arguments[:2]) == ["pr", "diff"]:
|
|
108
|
+
return ""
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_main_blocks_gh_pr_ready_without_proof_comment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
115
|
+
_install_fake_gh_responses(monkeypatch, [], ["src/parser.py"])
|
|
116
|
+
decision_output = _run_hook_main_with_command("gh pr ready 123")
|
|
117
|
+
assert "deny" in decision_output
|
|
118
|
+
assert "proof" in decision_output.lower()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_main_allows_gh_pr_ready_with_passing_proof_comment(
|
|
122
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
123
|
+
) -> None:
|
|
124
|
+
_install_fake_gh_responses(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"])
|
|
125
|
+
decision_output = _run_hook_main_with_command("gh pr ready 123")
|
|
126
|
+
assert "deny" not in decision_output
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_main_allows_complete_proof_comment_post(
|
|
130
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
131
|
+
) -> None:
|
|
132
|
+
_install_fake_gh_responses(monkeypatch, [], ["src/parser.py"])
|
|
133
|
+
body_file = tmp_path / "proof.md"
|
|
134
|
+
body_file.write_text(PASSING_PROOF_COMMENT_BODY)
|
|
135
|
+
decision_output = _run_hook_main_with_command(f"gh pr comment 123 --body-file {body_file}")
|
|
136
|
+
assert "deny" not in decision_output
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_main_blocks_proof_comment_missing_visual_on_visual_change(
|
|
140
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
141
|
+
) -> None:
|
|
142
|
+
_install_fake_gh_responses(monkeypatch, [], ["assets/icon.png"])
|
|
143
|
+
body_file = tmp_path / "proof.md"
|
|
144
|
+
body_file.write_text(PASSING_PROOF_COMMENT_BODY)
|
|
145
|
+
decision_output = _run_hook_main_with_command(f"gh pr comment 123 --body-file {body_file}")
|
|
146
|
+
assert "deny" in decision_output
|
|
147
|
+
assert "visual" in decision_output
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_main_leaves_ordinary_comment_untouched(
|
|
151
|
+
tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
|
|
152
|
+
) -> None:
|
|
153
|
+
all_recorded_calls: list[list[str]] = []
|
|
154
|
+
|
|
155
|
+
def _recording_run_gh_command(all_gh_arguments):
|
|
156
|
+
all_recorded_calls.append(list(all_gh_arguments))
|
|
157
|
+
|
|
158
|
+
monkeypatch.setattr(proof_module, "_run_gh_command", _recording_run_gh_command)
|
|
159
|
+
body_file = tmp_path / "comment.md"
|
|
160
|
+
body_file.write_text(ORDINARY_COMMENT_BODY)
|
|
161
|
+
decision_output = _run_hook_main_with_command(f"gh pr comment 123 --body-file {body_file}")
|
|
162
|
+
assert "deny" not in decision_output
|
|
163
|
+
assert all_recorded_calls == []
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_main_allows_gh_pr_ready_undo_without_queries(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
167
|
+
all_recorded_calls: list[list[str]] = []
|
|
168
|
+
|
|
169
|
+
def _recording_run_gh_command(all_gh_arguments):
|
|
170
|
+
all_recorded_calls.append(list(all_gh_arguments))
|
|
171
|
+
|
|
172
|
+
monkeypatch.setattr(proof_module, "_run_gh_command", _recording_run_gh_command)
|
|
173
|
+
decision_output = _run_hook_main_with_command("gh pr ready 123 --undo")
|
|
174
|
+
assert "deny" not in decision_output
|
|
175
|
+
assert all_recorded_calls == []
|
|
@@ -0,0 +1,162 @@
|
|
|
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 pr_description_proof_of_work as proof_module # noqa: E402
|
|
21
|
+
from blocking.pr_description_proof_of_work import ( # noqa: E402
|
|
22
|
+
audit_proof_comment_body,
|
|
23
|
+
evaluate_pr_ready_gate,
|
|
24
|
+
is_pr_ready_command,
|
|
25
|
+
is_proof_shaped_body,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
PASSING_PROOF_COMMENT_BODY = (
|
|
29
|
+
"## Summary\n\n"
|
|
30
|
+
"Recolored the launcher icons and checked every produced asset by hand "
|
|
31
|
+
"against the palette the plan names.\n\n"
|
|
32
|
+
"## Verification\n\n"
|
|
33
|
+
"```\n"
|
|
34
|
+
"C:\\Python313\\python.exe scripts/recolor.py --input themes/dawn\n"
|
|
35
|
+
"```\n\n"
|
|
36
|
+
"| Artifact | Measured |\n"
|
|
37
|
+
"| --- | --- |\n"
|
|
38
|
+
"| icons.zip | 48 files, 412,993 bytes |\n\n"
|
|
39
|
+
"This advances phase 2 of issue #12.\n\n"
|
|
40
|
+
"Known gap: the offline proof cannot show the on-device rendering; the "
|
|
41
|
+
"store preview covers that.\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
VISUAL_PROOF_COMMENT_BODY = (
|
|
45
|
+
PASSING_PROOF_COMMENT_BODY + "\n\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
BARE_PROOF_COMMENT_BODY = "## Verification\n\nLooks good overall to me.\n"
|
|
49
|
+
|
|
50
|
+
ORDINARY_COMMENT_BODY = (
|
|
51
|
+
"The retry logic here mirrors what the uploader does for chunked "
|
|
52
|
+
"transfers, so the two stay in step."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _install_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text):
|
|
57
|
+
all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
|
|
58
|
+
|
|
59
|
+
def _fake_run_gh_command(all_gh_arguments):
|
|
60
|
+
if all_gh_arguments and all_gh_arguments[0] == "api":
|
|
61
|
+
return json.dumps([all_comment_records])
|
|
62
|
+
if list(all_gh_arguments[:2]) == ["pr", "view"]:
|
|
63
|
+
return json.dumps({"number": 77})
|
|
64
|
+
if "--name-only" in all_gh_arguments:
|
|
65
|
+
return "\n".join(all_diff_names)
|
|
66
|
+
if list(all_gh_arguments[:2]) == ["pr", "diff"]:
|
|
67
|
+
return diff_text
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_is_proof_shaped_body_detects_verification_heading() -> None:
|
|
74
|
+
assert is_proof_shaped_body(PASSING_PROOF_COMMENT_BODY)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_is_proof_shaped_body_skips_ordinary_comment() -> None:
|
|
78
|
+
assert not is_proof_shaped_body(ORDINARY_COMMENT_BODY)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_is_pr_ready_command_matches_ready() -> None:
|
|
82
|
+
assert is_pr_ready_command("gh pr ready 123")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_is_pr_ready_command_skips_undo() -> None:
|
|
86
|
+
assert not is_pr_ready_command("gh pr ready 123 --undo")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_is_pr_ready_command_matches_after_shell_operator() -> None:
|
|
90
|
+
assert is_pr_ready_command("git fetch && gh pr ready 5")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_is_pr_ready_command_ignores_phrase_inside_commit_message() -> None:
|
|
94
|
+
assert not is_pr_ready_command(
|
|
95
|
+
'git commit -m "block gh pr ready without a proof comment"'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_is_pr_ready_command_ignores_phrase_inside_search_argument() -> None:
|
|
100
|
+
assert not is_pr_ready_command('grep -r "gh pr ready" packages/')
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_audit_passes_complete_proof_on_non_visual_change(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
104
|
+
_install_fake_gh(monkeypatch, [], ["src/parser.py"], "+ plain code line")
|
|
105
|
+
assert audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123) == []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_audit_flags_missing_visual_on_image_diff(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
109
|
+
_install_fake_gh(monkeypatch, [], ["assets/icon.png"], "")
|
|
110
|
+
all_missing_parts = audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123)
|
|
111
|
+
assert any("visual" in each_part for each_part in all_missing_parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_audit_accepts_image_embed_on_image_diff(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
115
|
+
_install_fake_gh(monkeypatch, [], ["assets/icon.png"], "")
|
|
116
|
+
assert audit_proof_comment_body(VISUAL_PROOF_COMMENT_BODY, 123) == []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_audit_detects_visual_change_from_hex_colors(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
120
|
+
_install_fake_gh(monkeypatch, [], ["src/theme.py"], '+ accent = "#AABBCC"')
|
|
121
|
+
all_missing_parts = audit_proof_comment_body(PASSING_PROOF_COMMENT_BODY, 123)
|
|
122
|
+
assert any("visual" in each_part for each_part in all_missing_parts)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_audit_names_every_missing_part_of_bare_proof(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
126
|
+
_install_fake_gh(monkeypatch, [], ["src/parser.py"], "")
|
|
127
|
+
all_missing_parts = audit_proof_comment_body(BARE_PROOF_COMMENT_BODY, 123)
|
|
128
|
+
assert len(all_missing_parts) == 4
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_ready_gate_blocks_when_no_proof_comment_exists(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
132
|
+
_install_fake_gh(monkeypatch, [ORDINARY_COMMENT_BODY], ["src/parser.py"], "")
|
|
133
|
+
denial_reason = evaluate_pr_ready_gate("gh pr ready 123")
|
|
134
|
+
assert denial_reason is not None
|
|
135
|
+
assert "proof" in denial_reason.lower()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_ready_gate_allows_with_passing_proof_comment(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
139
|
+
_install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
140
|
+
assert evaluate_pr_ready_gate("gh pr ready 123") is None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_ready_gate_blocks_when_proof_comment_is_incomplete(
|
|
144
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
145
|
+
) -> None:
|
|
146
|
+
_install_fake_gh(monkeypatch, [BARE_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
147
|
+
denial_reason = evaluate_pr_ready_gate("gh pr ready 123")
|
|
148
|
+
assert denial_reason is not None
|
|
149
|
+
assert "123" in denial_reason
|
|
150
|
+
assert "proof" in denial_reason.lower()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_ready_gate_fails_open_when_gh_is_unavailable(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
154
|
+
monkeypatch.setattr(proof_module, "_run_gh_command", lambda all_gh_arguments: None)
|
|
155
|
+
assert evaluate_pr_ready_gate("gh pr ready 123") is None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_ready_gate_resolves_pr_number_from_current_branch(
|
|
159
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
160
|
+
) -> None:
|
|
161
|
+
_install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
|
|
162
|
+
assert evaluate_pr_ready_gate("gh pr ready") is None
|
|
@@ -40,6 +40,73 @@ def compute_total() -> int:
|
|
|
40
40
|
return result
|
|
41
41
|
'''
|
|
42
42
|
|
|
43
|
+
WIDGET_MODULE_BASE_SOURCE = '''"""Arithmetic helpers used by the precommit gate worktree tests."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_one(number: int) -> int:
|
|
47
|
+
"""Return *number* plus one.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
number: The integer to increment.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The incremented integer.
|
|
54
|
+
"""
|
|
55
|
+
return number + 1
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
WIDGET_MODULE_EXPANDED_SOURCE = '''"""Arithmetic helpers used by the precommit gate worktree tests."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_one(number: int) -> int:
|
|
62
|
+
"""Return *number* plus one.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
number: The integer to increment.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The incremented integer.
|
|
69
|
+
"""
|
|
70
|
+
return number + 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def subtract_one(number: int) -> int:
|
|
74
|
+
"""Return *number* minus one.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
number: The integer to decrement.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The decremented integer.
|
|
81
|
+
"""
|
|
82
|
+
return number - 1
|
|
83
|
+
'''
|
|
84
|
+
|
|
85
|
+
WIDGET_TEST_BASE_SOURCE = '''"""Behavior tests for the widget arithmetic helpers."""
|
|
86
|
+
|
|
87
|
+
from widget import add_one
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_add_one_increments() -> None:
|
|
91
|
+
"""add_one returns its argument plus one."""
|
|
92
|
+
assert add_one(1) == 2
|
|
93
|
+
'''
|
|
94
|
+
|
|
95
|
+
WIDGET_TEST_EXPANDED_SOURCE = '''"""Behavior tests for the widget arithmetic helpers."""
|
|
96
|
+
|
|
97
|
+
from widget import add_one, subtract_one
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_add_one_increments() -> None:
|
|
101
|
+
"""add_one returns its argument plus one."""
|
|
102
|
+
assert add_one(1) == 2
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_subtract_one_decrements() -> None:
|
|
106
|
+
"""subtract_one returns its argument minus one."""
|
|
107
|
+
assert subtract_one(2) == 1
|
|
108
|
+
'''
|
|
109
|
+
|
|
43
110
|
|
|
44
111
|
def run_git(repository_root: Path, *git_arguments: str) -> None:
|
|
45
112
|
subprocess.run(
|
|
@@ -124,3 +191,25 @@ def test_commit_with_no_staged_python_files_is_allowed(tmp_path: Path) -> None:
|
|
|
124
191
|
completed_hook = run_hook("git commit -m docs", tmp_path)
|
|
125
192
|
assert completed_hook.returncode == 0
|
|
126
193
|
assert completed_hook.stdout.strip() == ""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_worktree_commit_reads_worktree_paired_tests_not_sibling_checkout(
|
|
197
|
+
tmp_path: Path,
|
|
198
|
+
) -> None:
|
|
199
|
+
main_checkout = tmp_path / "main_checkout"
|
|
200
|
+
main_checkout.mkdir()
|
|
201
|
+
initialize_repository(main_checkout)
|
|
202
|
+
(main_checkout / "widget.py").write_text(WIDGET_MODULE_BASE_SOURCE, encoding="utf-8")
|
|
203
|
+
(main_checkout / "test_widget.py").write_text(WIDGET_TEST_BASE_SOURCE, encoding="utf-8")
|
|
204
|
+
run_git(main_checkout, "add", "widget.py", "test_widget.py")
|
|
205
|
+
run_git(main_checkout, "commit", "-m", "base")
|
|
206
|
+
worktree_root = tmp_path / "sibling_worktree"
|
|
207
|
+
run_git(main_checkout, "worktree", "add", str(worktree_root), "-b", "feature")
|
|
208
|
+
stage_file(worktree_root, "widget.py", WIDGET_MODULE_EXPANDED_SOURCE)
|
|
209
|
+
stage_file(worktree_root, "test_widget.py", WIDGET_TEST_EXPANDED_SOURCE)
|
|
210
|
+
quoted_worktree = str(worktree_root)
|
|
211
|
+
completed_hook = run_hook(
|
|
212
|
+
f'git -C "{quoted_worktree}" commit -m expand', main_checkout
|
|
213
|
+
)
|
|
214
|
+
assert completed_hook.returncode == 0
|
|
215
|
+
assert completed_hook.stdout.strip() == ""
|
|
@@ -736,6 +736,10 @@ def test_hook_subprocess_imports_real_config_when_parent_holds_shadowing_config(
|
|
|
736
736
|
real_blocking_directory / "verdict_directory_write_blocker.py",
|
|
737
737
|
staged_blocking_directory / "verdict_directory_write_blocker.py",
|
|
738
738
|
)
|
|
739
|
+
shutil.copy(
|
|
740
|
+
real_blocking_directory / "verified_commit_config_bootstrap.py",
|
|
741
|
+
staged_blocking_directory / "verified_commit_config_bootstrap.py",
|
|
742
|
+
)
|
|
739
743
|
shutil.copytree(
|
|
740
744
|
real_blocking_directory / "config",
|
|
741
745
|
staged_blocking_directory / "config",
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import pathlib
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
@@ -785,3 +786,13 @@ def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
|
|
|
785
786
|
)
|
|
786
787
|
assert completed_process.returncode != 0
|
|
787
788
|
assert completed_process.stdout.strip() == ""
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def test_store_imports_under_foreign_config_shadow(
|
|
792
|
+
run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
|
|
793
|
+
) -> None:
|
|
794
|
+
completed_probe = run_under_config_shadow(
|
|
795
|
+
"import verification_verdict_store\nprint('IMPORT_OK')\n"
|
|
796
|
+
)
|
|
797
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
798
|
+
assert "IMPORT_OK" in completed_probe.stdout
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Regression tests for the verified-commit constants bootstrap.
|
|
2
|
+
|
|
3
|
+
These prove the shared constants module resolves to the sibling
|
|
4
|
+
``config/verified_commit_constants.py`` file even when a foreign ``config``
|
|
5
|
+
package sits ahead of ``blocking`` on ``sys.path``, and that a registration a
|
|
6
|
+
caller placed first is left untouched.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
RunUnderConfigShadow = Callable[[str], subprocess.CompletedProcess[str]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_register_binds_dotted_name_to_sibling_constants_file(
|
|
16
|
+
run_under_config_shadow: RunUnderConfigShadow,
|
|
17
|
+
) -> None:
|
|
18
|
+
completed_probe = run_under_config_shadow(
|
|
19
|
+
"import verified_commit_config_bootstrap as bootstrap\n"
|
|
20
|
+
"bootstrap.register_verified_commit_constants()\n"
|
|
21
|
+
"import config.verified_commit_constants as loaded\n"
|
|
22
|
+
"print(loaded.__file__)\n"
|
|
23
|
+
"print(loaded.DETACHED_HEAD_LABEL)\n"
|
|
24
|
+
)
|
|
25
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
26
|
+
assert "IS_DECOY_CONFIG" not in completed_probe.stdout
|
|
27
|
+
resolved_file_line, detached_head_line = completed_probe.stdout.splitlines()[:2]
|
|
28
|
+
assert resolved_file_line.replace("\\", "/").endswith(
|
|
29
|
+
"blocking/config/verified_commit_constants.py"
|
|
30
|
+
)
|
|
31
|
+
assert detached_head_line == "HEAD"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_register_leaves_an_already_cached_module_untouched(
|
|
35
|
+
run_under_config_shadow: RunUnderConfigShadow,
|
|
36
|
+
) -> None:
|
|
37
|
+
completed_probe = run_under_config_shadow(
|
|
38
|
+
"import sys\n"
|
|
39
|
+
"import types\n"
|
|
40
|
+
"sentinel = types.ModuleType('config.verified_commit_constants')\n"
|
|
41
|
+
"sentinel.ORIGIN = 'caller-seeded'\n"
|
|
42
|
+
"sys.modules['config.verified_commit_constants'] = sentinel\n"
|
|
43
|
+
"import verified_commit_config_bootstrap as bootstrap\n"
|
|
44
|
+
"bootstrap.register_verified_commit_constants()\n"
|
|
45
|
+
"import config.verified_commit_constants as loaded\n"
|
|
46
|
+
"print(getattr(loaded, 'ORIGIN', 'displaced'))\n"
|
|
47
|
+
)
|
|
48
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
49
|
+
assert completed_probe.stdout.strip().splitlines()[-1] == "caller-seeded"
|
|
@@ -12,6 +12,7 @@ import os
|
|
|
12
12
|
import pathlib
|
|
13
13
|
import subprocess
|
|
14
14
|
import sys
|
|
15
|
+
from collections.abc import Callable
|
|
15
16
|
|
|
16
17
|
import pytest
|
|
17
18
|
|
|
@@ -568,3 +569,13 @@ def test_minted_verdict_from_other_worktree_with_wrong_hash_denies(
|
|
|
568
569
|
deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
|
|
569
570
|
assert deny_reason is not None
|
|
570
571
|
assert "VERIFIED_COMMIT_GATE" in deny_reason
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_gate_imports_under_foreign_config_shadow(
|
|
575
|
+
run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
|
|
576
|
+
) -> None:
|
|
577
|
+
completed_probe = run_under_config_shadow(
|
|
578
|
+
"import verified_commit_gate\nprint('IMPORT_OK')\n"
|
|
579
|
+
)
|
|
580
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
581
|
+
assert "IMPORT_OK" in completed_probe.stdout
|
|
@@ -21,6 +21,7 @@ import json
|
|
|
21
21
|
import pathlib
|
|
22
22
|
import subprocess
|
|
23
23
|
import sys
|
|
24
|
+
from collections.abc import Callable
|
|
24
25
|
|
|
25
26
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
26
27
|
if str(_HOOK_DIR) not in sys.path:
|
|
@@ -389,3 +390,13 @@ def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -
|
|
|
389
390
|
finally:
|
|
390
391
|
if verdict_path is not None and verdict_path.exists():
|
|
391
392
|
verdict_path.unlink()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_minter_imports_under_foreign_config_shadow(
|
|
396
|
+
run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
|
|
397
|
+
) -> None:
|
|
398
|
+
completed_probe = run_under_config_shadow(
|
|
399
|
+
"import verifier_verdict_minter\nprint('IMPORT_OK')\n"
|
|
400
|
+
)
|
|
401
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
402
|
+
assert "IMPORT_OK" in completed_probe.stdout
|