claude-dev-env 2.4.0 → 2.5.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 +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +2 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +0 -1
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -1,507 +1,507 @@
|
|
|
1
|
-
"""Fixture-driven tests for the conditional Codex convergence gate.
|
|
2
|
-
|
|
3
|
-
Covers: required-and-clean, required-and-dirty, skipped-by-threshold,
|
|
4
|
-
skipped-by-token, skipped-by-down. Threshold comes from the probe constant
|
|
5
|
-
via ``is_codex_review_required`` — this module never inlines the percent.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import importlib.util
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from types import ModuleType
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
import _pr_converge_path_setup # noqa: F401
|
|
19
|
-
from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
20
|
-
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
|
|
21
|
-
)
|
|
22
|
-
from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
23
|
-
MINIMUM_ABBREVIATED_SHA_LENGTH,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
_SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
|
|
27
|
-
_PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
|
|
28
|
-
|
|
29
|
-
CLEAN_LABEL = "Clean \u2014 no findings"
|
|
30
|
-
CLEAN_BODY = (
|
|
31
|
-
f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
|
|
32
|
-
"No findings.\n"
|
|
33
|
-
)
|
|
34
|
-
HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
35
|
-
OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
36
|
-
PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
|
|
37
|
-
PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _load_module() -> ModuleType:
|
|
41
|
-
for each_cached_name in [
|
|
42
|
-
each_key
|
|
43
|
-
for each_key in list(sys.modules)
|
|
44
|
-
if each_key == "config" or each_key.startswith("config.")
|
|
45
|
-
]:
|
|
46
|
-
sys.modules.pop(each_cached_name, None)
|
|
47
|
-
if str(_PR_CONVERGE_DIRECTORY) in sys.path:
|
|
48
|
-
sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
|
|
49
|
-
sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
|
|
50
|
-
module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
|
|
51
|
-
spec = importlib.util.spec_from_file_location(
|
|
52
|
-
"check_convergence_codex_under_test", module_path
|
|
53
|
-
)
|
|
54
|
-
assert spec is not None
|
|
55
|
-
assert spec.loader is not None
|
|
56
|
-
module = importlib.util.module_from_spec(spec)
|
|
57
|
-
spec.loader.exec_module(module)
|
|
58
|
-
return module
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
check_convergence = _load_module()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _write_fixture(
|
|
65
|
-
tmp_path: Path,
|
|
66
|
-
*,
|
|
67
|
-
codex_percent_left: float | None,
|
|
68
|
-
codex_clean_at: str | None,
|
|
69
|
-
filename: str = "codex-gate.json",
|
|
70
|
-
) -> Path:
|
|
71
|
-
fixture_path = tmp_path / filename
|
|
72
|
-
payload: dict[str, object] = {
|
|
73
|
-
"head_sha": HEAD_SHA,
|
|
74
|
-
"pr_object": {"mergeable": True, "mergeable_state": "clean"},
|
|
75
|
-
"reviews": [
|
|
76
|
-
{
|
|
77
|
-
"id": 1,
|
|
78
|
-
"body": CLEAN_BODY,
|
|
79
|
-
"commit_id": HEAD_SHA,
|
|
80
|
-
}
|
|
81
|
-
],
|
|
82
|
-
"unresolved_bot_threads_passed": True,
|
|
83
|
-
"unresolved_bot_threads_detail": "0 unresolved",
|
|
84
|
-
"pending_reviews_passed": True,
|
|
85
|
-
"pending_reviews_detail": "none pending",
|
|
86
|
-
"codex_percent_left": codex_percent_left,
|
|
87
|
-
"codex_clean_at": codex_clean_at,
|
|
88
|
-
}
|
|
89
|
-
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
90
|
-
return fixture_path
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def should_pass_when_codex_required_and_clean(
|
|
94
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
95
|
-
) -> None:
|
|
96
|
-
fixture_path = _write_fixture(
|
|
97
|
-
tmp_path,
|
|
98
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
99
|
-
codex_clean_at=HEAD_SHA,
|
|
100
|
-
filename="required-clean.json",
|
|
101
|
-
)
|
|
102
|
-
exit_code = check_convergence.main(
|
|
103
|
-
[
|
|
104
|
-
"--owner",
|
|
105
|
-
"jl-cmd",
|
|
106
|
-
"--repo",
|
|
107
|
-
"claude-dev-env",
|
|
108
|
-
"--pr-number",
|
|
109
|
-
"111",
|
|
110
|
-
"--fixture",
|
|
111
|
-
str(fixture_path),
|
|
112
|
-
"--bugbot-down",
|
|
113
|
-
"--copilot-down",
|
|
114
|
-
]
|
|
115
|
-
)
|
|
116
|
-
captured = capsys.readouterr().out
|
|
117
|
-
assert exit_code == 0
|
|
118
|
-
assert "codex_clean_at == current_head: PASS" in captured
|
|
119
|
-
assert "clean at" in captured
|
|
120
|
-
assert "All pre-conditions met" in captured
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
|
|
124
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
125
|
-
) -> None:
|
|
126
|
-
def _probe_that_must_not_run() -> float | None:
|
|
127
|
-
raise AssertionError(
|
|
128
|
-
"the weekly usage probe must not run when codex_clean_at is on HEAD"
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
monkeypatch.setattr(
|
|
132
|
-
check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
is_passed, detail = check_convergence._evaluate_codex_clean(
|
|
136
|
-
read_percent_left=check_convergence._probe_codex_percent_left,
|
|
137
|
-
codex_clean_at=HEAD_SHA,
|
|
138
|
-
head_sha=HEAD_SHA,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
assert is_passed is True
|
|
142
|
-
assert "clean at" in detail
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def should_match_a_clean_stamp_whose_hex_case_differs_from_the_head_sha() -> None:
|
|
146
|
-
assert check_convergence._sha_matches_head(HEAD_SHA.upper(), HEAD_SHA) is True
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
|
|
150
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
151
|
-
) -> None:
|
|
152
|
-
fixture_path = _write_fixture(
|
|
153
|
-
tmp_path,
|
|
154
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
155
|
-
codex_clean_at=HEAD_SHA[:1],
|
|
156
|
-
filename="required-short-stamp.json",
|
|
157
|
-
)
|
|
158
|
-
exit_code = check_convergence.main(
|
|
159
|
-
[
|
|
160
|
-
"--owner",
|
|
161
|
-
"jl-cmd",
|
|
162
|
-
"--repo",
|
|
163
|
-
"claude-dev-env",
|
|
164
|
-
"--pr-number",
|
|
165
|
-
"111",
|
|
166
|
-
"--fixture",
|
|
167
|
-
str(fixture_path),
|
|
168
|
-
"--bugbot-down",
|
|
169
|
-
"--copilot-down",
|
|
170
|
-
]
|
|
171
|
-
)
|
|
172
|
-
captured = capsys.readouterr().out
|
|
173
|
-
assert exit_code == 1
|
|
174
|
-
assert "codex_clean_at == current_head: FAIL" in captured
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
|
|
178
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
179
|
-
) -> None:
|
|
180
|
-
fixture_path = _write_fixture(
|
|
181
|
-
tmp_path,
|
|
182
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
183
|
-
codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
|
|
184
|
-
filename="required-abbreviated-stamp.json",
|
|
185
|
-
)
|
|
186
|
-
exit_code = check_convergence.main(
|
|
187
|
-
[
|
|
188
|
-
"--owner",
|
|
189
|
-
"jl-cmd",
|
|
190
|
-
"--repo",
|
|
191
|
-
"claude-dev-env",
|
|
192
|
-
"--pr-number",
|
|
193
|
-
"111",
|
|
194
|
-
"--fixture",
|
|
195
|
-
str(fixture_path),
|
|
196
|
-
"--bugbot-down",
|
|
197
|
-
"--copilot-down",
|
|
198
|
-
]
|
|
199
|
-
)
|
|
200
|
-
captured = capsys.readouterr().out
|
|
201
|
-
assert exit_code == 0
|
|
202
|
-
assert "codex_clean_at == current_head: PASS" in captured
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def should_fail_when_codex_required_and_dirty(
|
|
206
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
207
|
-
) -> None:
|
|
208
|
-
fixture_path = _write_fixture(
|
|
209
|
-
tmp_path,
|
|
210
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
211
|
-
codex_clean_at=OTHER_SHA,
|
|
212
|
-
filename="required-dirty.json",
|
|
213
|
-
)
|
|
214
|
-
exit_code = check_convergence.main(
|
|
215
|
-
[
|
|
216
|
-
"--owner",
|
|
217
|
-
"jl-cmd",
|
|
218
|
-
"--repo",
|
|
219
|
-
"claude-dev-env",
|
|
220
|
-
"--pr-number",
|
|
221
|
-
"111",
|
|
222
|
-
"--fixture",
|
|
223
|
-
str(fixture_path),
|
|
224
|
-
"--bugbot-down",
|
|
225
|
-
"--copilot-down",
|
|
226
|
-
]
|
|
227
|
-
)
|
|
228
|
-
captured = capsys.readouterr().out
|
|
229
|
-
assert exit_code == 1
|
|
230
|
-
assert "codex_clean_at == current_head: FAIL" in captured
|
|
231
|
-
assert "no codex clean on" in captured
|
|
232
|
-
assert "do not mark ready" in captured
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def should_skip_when_usage_at_or_below_threshold(
|
|
236
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
237
|
-
) -> None:
|
|
238
|
-
fixture_path = _write_fixture(
|
|
239
|
-
tmp_path,
|
|
240
|
-
codex_percent_left=PERCENT_AT_THRESHOLD,
|
|
241
|
-
codex_clean_at=None,
|
|
242
|
-
filename="skipped-threshold.json",
|
|
243
|
-
)
|
|
244
|
-
exit_code = check_convergence.main(
|
|
245
|
-
[
|
|
246
|
-
"--owner",
|
|
247
|
-
"jl-cmd",
|
|
248
|
-
"--repo",
|
|
249
|
-
"claude-dev-env",
|
|
250
|
-
"--pr-number",
|
|
251
|
-
"111",
|
|
252
|
-
"--fixture",
|
|
253
|
-
str(fixture_path),
|
|
254
|
-
"--bugbot-down",
|
|
255
|
-
"--copilot-down",
|
|
256
|
-
]
|
|
257
|
-
)
|
|
258
|
-
captured = capsys.readouterr().out
|
|
259
|
-
assert exit_code == 0
|
|
260
|
-
assert "codex_clean_at == current_head: PASS" in captured
|
|
261
|
-
assert "skipped (codex review not required)" in captured
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def should_skip_when_codex_percent_left_is_null(
|
|
265
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
266
|
-
) -> None:
|
|
267
|
-
fixture_path = _write_fixture(
|
|
268
|
-
tmp_path,
|
|
269
|
-
codex_percent_left=None,
|
|
270
|
-
codex_clean_at=None,
|
|
271
|
-
filename="skipped-null.json",
|
|
272
|
-
)
|
|
273
|
-
exit_code = check_convergence.main(
|
|
274
|
-
[
|
|
275
|
-
"--owner",
|
|
276
|
-
"jl-cmd",
|
|
277
|
-
"--repo",
|
|
278
|
-
"claude-dev-env",
|
|
279
|
-
"--pr-number",
|
|
280
|
-
"111",
|
|
281
|
-
"--fixture",
|
|
282
|
-
str(fixture_path),
|
|
283
|
-
"--bugbot-down",
|
|
284
|
-
"--copilot-down",
|
|
285
|
-
]
|
|
286
|
-
)
|
|
287
|
-
captured = capsys.readouterr().out
|
|
288
|
-
assert exit_code == 0
|
|
289
|
-
assert "skipped (codex review not required)" in captured
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def should_bypass_when_codex_token_disables_reviewer(
|
|
293
|
-
tmp_path: Path,
|
|
294
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
295
|
-
capsys: pytest.CaptureFixture[str],
|
|
296
|
-
) -> None:
|
|
297
|
-
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
298
|
-
fixture_path = _write_fixture(
|
|
299
|
-
tmp_path,
|
|
300
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
301
|
-
codex_clean_at=None,
|
|
302
|
-
filename="skipped-token.json",
|
|
303
|
-
)
|
|
304
|
-
exit_code = check_convergence.main(
|
|
305
|
-
[
|
|
306
|
-
"--owner",
|
|
307
|
-
"jl-cmd",
|
|
308
|
-
"--repo",
|
|
309
|
-
"claude-dev-env",
|
|
310
|
-
"--pr-number",
|
|
311
|
-
"111",
|
|
312
|
-
"--fixture",
|
|
313
|
-
str(fixture_path),
|
|
314
|
-
"--bugbot-down",
|
|
315
|
-
"--copilot-down",
|
|
316
|
-
]
|
|
317
|
-
)
|
|
318
|
-
captured = capsys.readouterr().out
|
|
319
|
-
assert exit_code == 0
|
|
320
|
-
assert "bypassed (codex_down)" in captured
|
|
321
|
-
assert "no codex clean on" not in captured
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def should_bypass_when_codex_down_flag_set(
|
|
325
|
-
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
326
|
-
) -> None:
|
|
327
|
-
fixture_path = _write_fixture(
|
|
328
|
-
tmp_path,
|
|
329
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
330
|
-
codex_clean_at=None,
|
|
331
|
-
filename="skipped-down.json",
|
|
332
|
-
)
|
|
333
|
-
exit_code = check_convergence.main(
|
|
334
|
-
[
|
|
335
|
-
"--owner",
|
|
336
|
-
"jl-cmd",
|
|
337
|
-
"--repo",
|
|
338
|
-
"claude-dev-env",
|
|
339
|
-
"--pr-number",
|
|
340
|
-
"111",
|
|
341
|
-
"--fixture",
|
|
342
|
-
str(fixture_path),
|
|
343
|
-
"--bugbot-down",
|
|
344
|
-
"--copilot-down",
|
|
345
|
-
"--codex-down",
|
|
346
|
-
]
|
|
347
|
-
)
|
|
348
|
-
captured = capsys.readouterr().out
|
|
349
|
-
assert exit_code == 0
|
|
350
|
-
assert "bypassed (codex_down)" in captured
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
def should_resolve_codex_down_true_when_flag_set(
|
|
354
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
355
|
-
) -> None:
|
|
356
|
-
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
357
|
-
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
358
|
-
assert check_convergence._resolve_codex_down(
|
|
359
|
-
True, check_convergence._read_job_state()
|
|
360
|
-
) is True
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
def should_resolve_codex_down_true_when_env_disables_codex(
|
|
364
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
365
|
-
) -> None:
|
|
366
|
-
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
367
|
-
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
368
|
-
assert check_convergence._resolve_codex_down(
|
|
369
|
-
False, check_convergence._read_job_state()
|
|
370
|
-
) is True
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def should_resolve_codex_down_false_when_flag_unset_and_env_empty(
|
|
374
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
375
|
-
) -> None:
|
|
376
|
-
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
377
|
-
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
378
|
-
assert check_convergence._resolve_codex_down(
|
|
379
|
-
False, check_convergence._read_job_state()
|
|
380
|
-
) is False
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def should_resolve_codex_down_true_when_job_state_sticky_down(
|
|
384
|
-
tmp_path: Path,
|
|
385
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
386
|
-
) -> None:
|
|
387
|
-
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
388
|
-
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
389
|
-
(tmp_path / "pr-converge-state.json").write_text(
|
|
390
|
-
json.dumps({"codex_down": True}),
|
|
391
|
-
encoding="utf-8",
|
|
392
|
-
)
|
|
393
|
-
assert check_convergence._resolve_codex_down(
|
|
394
|
-
False, check_convergence._read_job_state()
|
|
395
|
-
) is True
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def should_resolve_codex_down_false_when_job_state_codex_down_is_false(
|
|
399
|
-
tmp_path: Path,
|
|
400
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
401
|
-
) -> None:
|
|
402
|
-
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
403
|
-
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
404
|
-
(tmp_path / "pr-converge-state.json").write_text(
|
|
405
|
-
json.dumps({"codex_down": False}),
|
|
406
|
-
encoding="utf-8",
|
|
407
|
-
)
|
|
408
|
-
assert check_convergence._resolve_codex_down(
|
|
409
|
-
False, check_convergence._read_job_state()
|
|
410
|
-
) is False
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def should_bypass_when_job_state_codex_down_is_true(
|
|
414
|
-
tmp_path: Path,
|
|
415
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
416
|
-
capsys: pytest.CaptureFixture[str],
|
|
417
|
-
) -> None:
|
|
418
|
-
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
419
|
-
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
420
|
-
(tmp_path / "pr-converge-state.json").write_text(
|
|
421
|
-
json.dumps({"codex_down": True}),
|
|
422
|
-
encoding="utf-8",
|
|
423
|
-
)
|
|
424
|
-
fixture_path = _write_fixture(
|
|
425
|
-
tmp_path,
|
|
426
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
427
|
-
codex_clean_at=None,
|
|
428
|
-
filename="job-state-codex-down.json",
|
|
429
|
-
)
|
|
430
|
-
exit_code = check_convergence.main(
|
|
431
|
-
[
|
|
432
|
-
"--owner",
|
|
433
|
-
"jl-cmd",
|
|
434
|
-
"--repo",
|
|
435
|
-
"claude-dev-env",
|
|
436
|
-
"--pr-number",
|
|
437
|
-
"111",
|
|
438
|
-
"--fixture",
|
|
439
|
-
str(fixture_path),
|
|
440
|
-
"--bugbot-down",
|
|
441
|
-
"--copilot-down",
|
|
442
|
-
]
|
|
443
|
-
)
|
|
444
|
-
captured = capsys.readouterr().out
|
|
445
|
-
assert exit_code == 0
|
|
446
|
-
assert "bypassed (codex_down)" in captured
|
|
447
|
-
assert "no codex clean on" not in captured
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
|
|
451
|
-
tmp_path: Path,
|
|
452
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
453
|
-
capsys: pytest.CaptureFixture[str],
|
|
454
|
-
) -> None:
|
|
455
|
-
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
456
|
-
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
457
|
-
(tmp_path / "pr-converge-state.json").write_text(
|
|
458
|
-
json.dumps({}),
|
|
459
|
-
encoding="utf-8",
|
|
460
|
-
)
|
|
461
|
-
fixture_path = _write_fixture(
|
|
462
|
-
tmp_path,
|
|
463
|
-
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
464
|
-
codex_clean_at=None,
|
|
465
|
-
filename="job-state-no-down.json",
|
|
466
|
-
)
|
|
467
|
-
exit_code = check_convergence.main(
|
|
468
|
-
[
|
|
469
|
-
"--owner",
|
|
470
|
-
"jl-cmd",
|
|
471
|
-
"--repo",
|
|
472
|
-
"claude-dev-env",
|
|
473
|
-
"--pr-number",
|
|
474
|
-
"111",
|
|
475
|
-
"--fixture",
|
|
476
|
-
str(fixture_path),
|
|
477
|
-
"--bugbot-down",
|
|
478
|
-
"--copilot-down",
|
|
479
|
-
]
|
|
480
|
-
)
|
|
481
|
-
captured = capsys.readouterr().out
|
|
482
|
-
assert exit_code == 1
|
|
483
|
-
assert "codex_clean_at == current_head: FAIL" in captured
|
|
484
|
-
assert "no codex clean on" in captured
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
def should_accept_codex_down_flag_in_parsed_arguments() -> None:
|
|
488
|
-
arguments = check_convergence.parse_arguments(
|
|
489
|
-
["--owner", "o", "--repo", "r", "--pr-number", "1", "--codex-down"]
|
|
490
|
-
)
|
|
491
|
-
assert arguments.codex_down is True
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
|
|
495
|
-
arguments = check_convergence.parse_arguments(
|
|
496
|
-
[
|
|
497
|
-
"--owner",
|
|
498
|
-
"o",
|
|
499
|
-
"--repo",
|
|
500
|
-
"r",
|
|
501
|
-
"--pr-number",
|
|
502
|
-
"1",
|
|
503
|
-
"--codex-clean-at",
|
|
504
|
-
HEAD_SHA,
|
|
505
|
-
]
|
|
506
|
-
)
|
|
507
|
-
assert arguments.codex_clean_at == HEAD_SHA
|
|
1
|
+
"""Fixture-driven tests for the conditional Codex convergence gate.
|
|
2
|
+
|
|
3
|
+
Covers: required-and-clean, required-and-dirty, skipped-by-threshold,
|
|
4
|
+
skipped-by-token, skipped-by-down. Threshold comes from the probe constant
|
|
5
|
+
via ``is_codex_review_required`` — this module never inlines the percent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
import _pr_converge_path_setup # noqa: F401
|
|
19
|
+
from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
20
|
+
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
|
|
21
|
+
)
|
|
22
|
+
from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
23
|
+
MINIMUM_ABBREVIATED_SHA_LENGTH,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
|
|
27
|
+
_PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
|
|
28
|
+
|
|
29
|
+
CLEAN_LABEL = "Clean \u2014 no findings"
|
|
30
|
+
CLEAN_BODY = (
|
|
31
|
+
f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
|
|
32
|
+
"No findings.\n"
|
|
33
|
+
)
|
|
34
|
+
HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
35
|
+
OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
36
|
+
PERCENT_ABOVE_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT) + 1.0
|
|
37
|
+
PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_module() -> ModuleType:
|
|
41
|
+
for each_cached_name in [
|
|
42
|
+
each_key
|
|
43
|
+
for each_key in list(sys.modules)
|
|
44
|
+
if each_key == "config" or each_key.startswith("config.")
|
|
45
|
+
]:
|
|
46
|
+
sys.modules.pop(each_cached_name, None)
|
|
47
|
+
if str(_PR_CONVERGE_DIRECTORY) in sys.path:
|
|
48
|
+
sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
|
|
49
|
+
sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
|
|
50
|
+
module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
|
|
51
|
+
spec = importlib.util.spec_from_file_location(
|
|
52
|
+
"check_convergence_codex_under_test", module_path
|
|
53
|
+
)
|
|
54
|
+
assert spec is not None
|
|
55
|
+
assert spec.loader is not None
|
|
56
|
+
module = importlib.util.module_from_spec(spec)
|
|
57
|
+
spec.loader.exec_module(module)
|
|
58
|
+
return module
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
check_convergence = _load_module()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _write_fixture(
|
|
65
|
+
tmp_path: Path,
|
|
66
|
+
*,
|
|
67
|
+
codex_percent_left: float | None,
|
|
68
|
+
codex_clean_at: str | None,
|
|
69
|
+
filename: str = "codex-gate.json",
|
|
70
|
+
) -> Path:
|
|
71
|
+
fixture_path = tmp_path / filename
|
|
72
|
+
payload: dict[str, object] = {
|
|
73
|
+
"head_sha": HEAD_SHA,
|
|
74
|
+
"pr_object": {"mergeable": True, "mergeable_state": "clean"},
|
|
75
|
+
"reviews": [
|
|
76
|
+
{
|
|
77
|
+
"id": 1,
|
|
78
|
+
"body": CLEAN_BODY,
|
|
79
|
+
"commit_id": HEAD_SHA,
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"unresolved_bot_threads_passed": True,
|
|
83
|
+
"unresolved_bot_threads_detail": "0 unresolved",
|
|
84
|
+
"pending_reviews_passed": True,
|
|
85
|
+
"pending_reviews_detail": "none pending",
|
|
86
|
+
"codex_percent_left": codex_percent_left,
|
|
87
|
+
"codex_clean_at": codex_clean_at,
|
|
88
|
+
}
|
|
89
|
+
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
90
|
+
return fixture_path
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def should_pass_when_codex_required_and_clean(
|
|
94
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
95
|
+
) -> None:
|
|
96
|
+
fixture_path = _write_fixture(
|
|
97
|
+
tmp_path,
|
|
98
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
99
|
+
codex_clean_at=HEAD_SHA,
|
|
100
|
+
filename="required-clean.json",
|
|
101
|
+
)
|
|
102
|
+
exit_code = check_convergence.main(
|
|
103
|
+
[
|
|
104
|
+
"--owner",
|
|
105
|
+
"jl-cmd",
|
|
106
|
+
"--repo",
|
|
107
|
+
"claude-dev-env",
|
|
108
|
+
"--pr-number",
|
|
109
|
+
"111",
|
|
110
|
+
"--fixture",
|
|
111
|
+
str(fixture_path),
|
|
112
|
+
"--bugbot-down",
|
|
113
|
+
"--copilot-down",
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
captured = capsys.readouterr().out
|
|
117
|
+
assert exit_code == 0
|
|
118
|
+
assert "codex_clean_at == current_head: PASS" in captured
|
|
119
|
+
assert "clean at" in captured
|
|
120
|
+
assert "All pre-conditions met" in captured
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def should_pass_without_reading_usage_when_the_clean_stamp_is_on_head(
|
|
124
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
125
|
+
) -> None:
|
|
126
|
+
def _probe_that_must_not_run() -> float | None:
|
|
127
|
+
raise AssertionError(
|
|
128
|
+
"the weekly usage probe must not run when codex_clean_at is on HEAD"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
monkeypatch.setattr(
|
|
132
|
+
check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
is_passed, detail = check_convergence._evaluate_codex_clean(
|
|
136
|
+
read_percent_left=check_convergence._probe_codex_percent_left,
|
|
137
|
+
codex_clean_at=HEAD_SHA,
|
|
138
|
+
head_sha=HEAD_SHA,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert is_passed is True
|
|
142
|
+
assert "clean at" in detail
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def should_match_a_clean_stamp_whose_hex_case_differs_from_the_head_sha() -> None:
|
|
146
|
+
assert check_convergence._sha_matches_head(HEAD_SHA.upper(), HEAD_SHA) is True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
|
|
150
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
151
|
+
) -> None:
|
|
152
|
+
fixture_path = _write_fixture(
|
|
153
|
+
tmp_path,
|
|
154
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
155
|
+
codex_clean_at=HEAD_SHA[:1],
|
|
156
|
+
filename="required-short-stamp.json",
|
|
157
|
+
)
|
|
158
|
+
exit_code = check_convergence.main(
|
|
159
|
+
[
|
|
160
|
+
"--owner",
|
|
161
|
+
"jl-cmd",
|
|
162
|
+
"--repo",
|
|
163
|
+
"claude-dev-env",
|
|
164
|
+
"--pr-number",
|
|
165
|
+
"111",
|
|
166
|
+
"--fixture",
|
|
167
|
+
str(fixture_path),
|
|
168
|
+
"--bugbot-down",
|
|
169
|
+
"--copilot-down",
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
captured = capsys.readouterr().out
|
|
173
|
+
assert exit_code == 1
|
|
174
|
+
assert "codex_clean_at == current_head: FAIL" in captured
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
|
|
178
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
179
|
+
) -> None:
|
|
180
|
+
fixture_path = _write_fixture(
|
|
181
|
+
tmp_path,
|
|
182
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
183
|
+
codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
|
|
184
|
+
filename="required-abbreviated-stamp.json",
|
|
185
|
+
)
|
|
186
|
+
exit_code = check_convergence.main(
|
|
187
|
+
[
|
|
188
|
+
"--owner",
|
|
189
|
+
"jl-cmd",
|
|
190
|
+
"--repo",
|
|
191
|
+
"claude-dev-env",
|
|
192
|
+
"--pr-number",
|
|
193
|
+
"111",
|
|
194
|
+
"--fixture",
|
|
195
|
+
str(fixture_path),
|
|
196
|
+
"--bugbot-down",
|
|
197
|
+
"--copilot-down",
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
captured = capsys.readouterr().out
|
|
201
|
+
assert exit_code == 0
|
|
202
|
+
assert "codex_clean_at == current_head: PASS" in captured
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def should_fail_when_codex_required_and_dirty(
|
|
206
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
207
|
+
) -> None:
|
|
208
|
+
fixture_path = _write_fixture(
|
|
209
|
+
tmp_path,
|
|
210
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
211
|
+
codex_clean_at=OTHER_SHA,
|
|
212
|
+
filename="required-dirty.json",
|
|
213
|
+
)
|
|
214
|
+
exit_code = check_convergence.main(
|
|
215
|
+
[
|
|
216
|
+
"--owner",
|
|
217
|
+
"jl-cmd",
|
|
218
|
+
"--repo",
|
|
219
|
+
"claude-dev-env",
|
|
220
|
+
"--pr-number",
|
|
221
|
+
"111",
|
|
222
|
+
"--fixture",
|
|
223
|
+
str(fixture_path),
|
|
224
|
+
"--bugbot-down",
|
|
225
|
+
"--copilot-down",
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
captured = capsys.readouterr().out
|
|
229
|
+
assert exit_code == 1
|
|
230
|
+
assert "codex_clean_at == current_head: FAIL" in captured
|
|
231
|
+
assert "no codex clean on" in captured
|
|
232
|
+
assert "do not mark ready" in captured
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def should_skip_when_usage_at_or_below_threshold(
|
|
236
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
237
|
+
) -> None:
|
|
238
|
+
fixture_path = _write_fixture(
|
|
239
|
+
tmp_path,
|
|
240
|
+
codex_percent_left=PERCENT_AT_THRESHOLD,
|
|
241
|
+
codex_clean_at=None,
|
|
242
|
+
filename="skipped-threshold.json",
|
|
243
|
+
)
|
|
244
|
+
exit_code = check_convergence.main(
|
|
245
|
+
[
|
|
246
|
+
"--owner",
|
|
247
|
+
"jl-cmd",
|
|
248
|
+
"--repo",
|
|
249
|
+
"claude-dev-env",
|
|
250
|
+
"--pr-number",
|
|
251
|
+
"111",
|
|
252
|
+
"--fixture",
|
|
253
|
+
str(fixture_path),
|
|
254
|
+
"--bugbot-down",
|
|
255
|
+
"--copilot-down",
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
captured = capsys.readouterr().out
|
|
259
|
+
assert exit_code == 0
|
|
260
|
+
assert "codex_clean_at == current_head: PASS" in captured
|
|
261
|
+
assert "skipped (codex review not required)" in captured
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def should_skip_when_codex_percent_left_is_null(
|
|
265
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
266
|
+
) -> None:
|
|
267
|
+
fixture_path = _write_fixture(
|
|
268
|
+
tmp_path,
|
|
269
|
+
codex_percent_left=None,
|
|
270
|
+
codex_clean_at=None,
|
|
271
|
+
filename="skipped-null.json",
|
|
272
|
+
)
|
|
273
|
+
exit_code = check_convergence.main(
|
|
274
|
+
[
|
|
275
|
+
"--owner",
|
|
276
|
+
"jl-cmd",
|
|
277
|
+
"--repo",
|
|
278
|
+
"claude-dev-env",
|
|
279
|
+
"--pr-number",
|
|
280
|
+
"111",
|
|
281
|
+
"--fixture",
|
|
282
|
+
str(fixture_path),
|
|
283
|
+
"--bugbot-down",
|
|
284
|
+
"--copilot-down",
|
|
285
|
+
]
|
|
286
|
+
)
|
|
287
|
+
captured = capsys.readouterr().out
|
|
288
|
+
assert exit_code == 0
|
|
289
|
+
assert "skipped (codex review not required)" in captured
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def should_bypass_when_codex_token_disables_reviewer(
|
|
293
|
+
tmp_path: Path,
|
|
294
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
295
|
+
capsys: pytest.CaptureFixture[str],
|
|
296
|
+
) -> None:
|
|
297
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
298
|
+
fixture_path = _write_fixture(
|
|
299
|
+
tmp_path,
|
|
300
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
301
|
+
codex_clean_at=None,
|
|
302
|
+
filename="skipped-token.json",
|
|
303
|
+
)
|
|
304
|
+
exit_code = check_convergence.main(
|
|
305
|
+
[
|
|
306
|
+
"--owner",
|
|
307
|
+
"jl-cmd",
|
|
308
|
+
"--repo",
|
|
309
|
+
"claude-dev-env",
|
|
310
|
+
"--pr-number",
|
|
311
|
+
"111",
|
|
312
|
+
"--fixture",
|
|
313
|
+
str(fixture_path),
|
|
314
|
+
"--bugbot-down",
|
|
315
|
+
"--copilot-down",
|
|
316
|
+
]
|
|
317
|
+
)
|
|
318
|
+
captured = capsys.readouterr().out
|
|
319
|
+
assert exit_code == 0
|
|
320
|
+
assert "bypassed (codex_down)" in captured
|
|
321
|
+
assert "no codex clean on" not in captured
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def should_bypass_when_codex_down_flag_set(
|
|
325
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
326
|
+
) -> None:
|
|
327
|
+
fixture_path = _write_fixture(
|
|
328
|
+
tmp_path,
|
|
329
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
330
|
+
codex_clean_at=None,
|
|
331
|
+
filename="skipped-down.json",
|
|
332
|
+
)
|
|
333
|
+
exit_code = check_convergence.main(
|
|
334
|
+
[
|
|
335
|
+
"--owner",
|
|
336
|
+
"jl-cmd",
|
|
337
|
+
"--repo",
|
|
338
|
+
"claude-dev-env",
|
|
339
|
+
"--pr-number",
|
|
340
|
+
"111",
|
|
341
|
+
"--fixture",
|
|
342
|
+
str(fixture_path),
|
|
343
|
+
"--bugbot-down",
|
|
344
|
+
"--copilot-down",
|
|
345
|
+
"--codex-down",
|
|
346
|
+
]
|
|
347
|
+
)
|
|
348
|
+
captured = capsys.readouterr().out
|
|
349
|
+
assert exit_code == 0
|
|
350
|
+
assert "bypassed (codex_down)" in captured
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def should_resolve_codex_down_true_when_flag_set(
|
|
354
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
355
|
+
) -> None:
|
|
356
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
357
|
+
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
358
|
+
assert check_convergence._resolve_codex_down(
|
|
359
|
+
True, check_convergence._read_job_state()
|
|
360
|
+
) is True
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def should_resolve_codex_down_true_when_env_disables_codex(
|
|
364
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
365
|
+
) -> None:
|
|
366
|
+
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
367
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
368
|
+
assert check_convergence._resolve_codex_down(
|
|
369
|
+
False, check_convergence._read_job_state()
|
|
370
|
+
) is True
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def should_resolve_codex_down_false_when_flag_unset_and_env_empty(
|
|
374
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
375
|
+
) -> None:
|
|
376
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
377
|
+
monkeypatch.delenv("CLAUDE_JOB_DIR", raising=False)
|
|
378
|
+
assert check_convergence._resolve_codex_down(
|
|
379
|
+
False, check_convergence._read_job_state()
|
|
380
|
+
) is False
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def should_resolve_codex_down_true_when_job_state_sticky_down(
|
|
384
|
+
tmp_path: Path,
|
|
385
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
386
|
+
) -> None:
|
|
387
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
388
|
+
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
389
|
+
(tmp_path / "pr-converge-state.json").write_text(
|
|
390
|
+
json.dumps({"codex_down": True}),
|
|
391
|
+
encoding="utf-8",
|
|
392
|
+
)
|
|
393
|
+
assert check_convergence._resolve_codex_down(
|
|
394
|
+
False, check_convergence._read_job_state()
|
|
395
|
+
) is True
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def should_resolve_codex_down_false_when_job_state_codex_down_is_false(
|
|
399
|
+
tmp_path: Path,
|
|
400
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
401
|
+
) -> None:
|
|
402
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
403
|
+
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
404
|
+
(tmp_path / "pr-converge-state.json").write_text(
|
|
405
|
+
json.dumps({"codex_down": False}),
|
|
406
|
+
encoding="utf-8",
|
|
407
|
+
)
|
|
408
|
+
assert check_convergence._resolve_codex_down(
|
|
409
|
+
False, check_convergence._read_job_state()
|
|
410
|
+
) is False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def should_bypass_when_job_state_codex_down_is_true(
|
|
414
|
+
tmp_path: Path,
|
|
415
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
416
|
+
capsys: pytest.CaptureFixture[str],
|
|
417
|
+
) -> None:
|
|
418
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
419
|
+
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
420
|
+
(tmp_path / "pr-converge-state.json").write_text(
|
|
421
|
+
json.dumps({"codex_down": True}),
|
|
422
|
+
encoding="utf-8",
|
|
423
|
+
)
|
|
424
|
+
fixture_path = _write_fixture(
|
|
425
|
+
tmp_path,
|
|
426
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
427
|
+
codex_clean_at=None,
|
|
428
|
+
filename="job-state-codex-down.json",
|
|
429
|
+
)
|
|
430
|
+
exit_code = check_convergence.main(
|
|
431
|
+
[
|
|
432
|
+
"--owner",
|
|
433
|
+
"jl-cmd",
|
|
434
|
+
"--repo",
|
|
435
|
+
"claude-dev-env",
|
|
436
|
+
"--pr-number",
|
|
437
|
+
"111",
|
|
438
|
+
"--fixture",
|
|
439
|
+
str(fixture_path),
|
|
440
|
+
"--bugbot-down",
|
|
441
|
+
"--copilot-down",
|
|
442
|
+
]
|
|
443
|
+
)
|
|
444
|
+
captured = capsys.readouterr().out
|
|
445
|
+
assert exit_code == 0
|
|
446
|
+
assert "bypassed (codex_down)" in captured
|
|
447
|
+
assert "no codex clean on" not in captured
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
|
|
451
|
+
tmp_path: Path,
|
|
452
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
453
|
+
capsys: pytest.CaptureFixture[str],
|
|
454
|
+
) -> None:
|
|
455
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
456
|
+
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path))
|
|
457
|
+
(tmp_path / "pr-converge-state.json").write_text(
|
|
458
|
+
json.dumps({}),
|
|
459
|
+
encoding="utf-8",
|
|
460
|
+
)
|
|
461
|
+
fixture_path = _write_fixture(
|
|
462
|
+
tmp_path,
|
|
463
|
+
codex_percent_left=PERCENT_ABOVE_THRESHOLD,
|
|
464
|
+
codex_clean_at=None,
|
|
465
|
+
filename="job-state-no-down.json",
|
|
466
|
+
)
|
|
467
|
+
exit_code = check_convergence.main(
|
|
468
|
+
[
|
|
469
|
+
"--owner",
|
|
470
|
+
"jl-cmd",
|
|
471
|
+
"--repo",
|
|
472
|
+
"claude-dev-env",
|
|
473
|
+
"--pr-number",
|
|
474
|
+
"111",
|
|
475
|
+
"--fixture",
|
|
476
|
+
str(fixture_path),
|
|
477
|
+
"--bugbot-down",
|
|
478
|
+
"--copilot-down",
|
|
479
|
+
]
|
|
480
|
+
)
|
|
481
|
+
captured = capsys.readouterr().out
|
|
482
|
+
assert exit_code == 1
|
|
483
|
+
assert "codex_clean_at == current_head: FAIL" in captured
|
|
484
|
+
assert "no codex clean on" in captured
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def should_accept_codex_down_flag_in_parsed_arguments() -> None:
|
|
488
|
+
arguments = check_convergence.parse_arguments(
|
|
489
|
+
["--owner", "o", "--repo", "r", "--pr-number", "1", "--codex-down"]
|
|
490
|
+
)
|
|
491
|
+
assert arguments.codex_down is True
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
|
|
495
|
+
arguments = check_convergence.parse_arguments(
|
|
496
|
+
[
|
|
497
|
+
"--owner",
|
|
498
|
+
"o",
|
|
499
|
+
"--repo",
|
|
500
|
+
"r",
|
|
501
|
+
"--pr-number",
|
|
502
|
+
"1",
|
|
503
|
+
"--codex-clean-at",
|
|
504
|
+
HEAD_SHA,
|
|
505
|
+
]
|
|
506
|
+
)
|
|
507
|
+
assert arguments.codex_clean_at == HEAD_SHA
|