claude-dev-env 1.94.0 → 2.0.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/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +41 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
- package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/CLAUDE.md +10 -1
- package/bin/ever-shipped-skills.mjs +73 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +140 -7
- package/bin/install.prune.test.mjs +457 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +83 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +97 -601
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +52 -4
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +202 -18
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +32 -22
- package/skills/orchestrator-refresh/SKILL.md +12 -8
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-fix-protocol/SKILL.md +65 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/skill-builder/references/skill-modularity.md +1 -1
- package/skills/team-advisor/SKILL.md +16 -12
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
::
|
|
4
4
|
|
|
5
|
-
check_all(all gates passing
|
|
6
|
-
|
|
5
|
+
check_all(all gates passing,
|
|
6
|
+
is_bugteam_post_blocked=False,
|
|
7
|
+
) -> exit 0, "All pre-conditions met ..."
|
|
8
|
+
check_all(one gate failing,
|
|
9
|
+
is_bugteam_post_blocked=False,
|
|
10
|
+
) -> exit 1, "One or more ... do not mark ready."
|
|
7
11
|
_get_pr_head_sha raises SystemExit -> exit 2 propagates
|
|
8
12
|
parse_arguments([... --bugteam-post-blocked]) -> bugteam_post_blocked True
|
|
9
13
|
|
|
@@ -22,6 +26,7 @@ from types import ModuleType
|
|
|
22
26
|
import pytest
|
|
23
27
|
|
|
24
28
|
import _pr_converge_path_setup # noqa: F401
|
|
29
|
+
import check_convergence_availability
|
|
25
30
|
from pr_converge_skill_constants.constants import EXIT_CODE_GH_ERROR
|
|
26
31
|
|
|
27
32
|
_SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
|
|
@@ -45,9 +50,10 @@ EXPECTED_ALL_PASS_STDOUT = (
|
|
|
45
50
|
"2. bugbot review body clean: PASS — clean\n"
|
|
46
51
|
"3. bugteam_clean_at == current_head: PASS — clean\n"
|
|
47
52
|
"4. copilot_clean_at == current_head: PASS — clean\n"
|
|
48
|
-
"5.
|
|
49
|
-
"6.
|
|
50
|
-
"7.
|
|
53
|
+
"5. codex_clean_at == current_head: PASS — skipped (codex review not required)\n"
|
|
54
|
+
"6. zero unresolved bot threads: PASS — clean\n"
|
|
55
|
+
"7. PR is mergeable: PASS — clean\n"
|
|
56
|
+
"8. no pending requested reviews: PASS — clean\n\n"
|
|
51
57
|
"All pre-conditions met — PR is ready to mark ready.\n"
|
|
52
58
|
)
|
|
53
59
|
|
|
@@ -56,9 +62,10 @@ EXPECTED_BUGBOT_DOWN_STDOUT = (
|
|
|
56
62
|
"1. bugbot_clean_at == current_head: PASS — bypassed (bugbot_down)\n"
|
|
57
63
|
"2. bugteam_clean_at == current_head: PASS — clean\n"
|
|
58
64
|
"3. copilot_clean_at == current_head: PASS — clean\n"
|
|
59
|
-
"4.
|
|
60
|
-
"5.
|
|
61
|
-
"6.
|
|
65
|
+
"4. codex_clean_at == current_head: PASS — skipped (codex review not required)\n"
|
|
66
|
+
"5. zero unresolved bot threads: PASS — clean\n"
|
|
67
|
+
"6. PR is mergeable: PASS — clean\n"
|
|
68
|
+
"7. no pending requested reviews: PASS — clean\n\n"
|
|
62
69
|
"All pre-conditions met — PR is ready to mark ready.\n"
|
|
63
70
|
)
|
|
64
71
|
|
|
@@ -68,9 +75,10 @@ EXPECTED_COPILOT_DOWN_STDOUT = (
|
|
|
68
75
|
"2. bugbot review body clean: PASS — clean\n"
|
|
69
76
|
"3. bugteam_clean_at == current_head: PASS — clean\n"
|
|
70
77
|
"4. copilot_clean_at == current_head: PASS — bypassed (copilot_down)\n"
|
|
71
|
-
"5.
|
|
72
|
-
"6.
|
|
73
|
-
"7.
|
|
78
|
+
"5. codex_clean_at == current_head: PASS — skipped (codex review not required)\n"
|
|
79
|
+
"6. zero unresolved bot threads: PASS — clean\n"
|
|
80
|
+
"7. PR is mergeable: PASS — clean\n"
|
|
81
|
+
"8. no pending requested reviews: PASS — bypassed (copilot_down)\n\n"
|
|
74
82
|
"All pre-conditions met — PR is ready to mark ready.\n"
|
|
75
83
|
)
|
|
76
84
|
|
|
@@ -110,8 +118,15 @@ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
|
|
|
110
118
|
)
|
|
111
119
|
|
|
112
120
|
|
|
121
|
+
def _null_codex_percent_left() -> float | None:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
113
125
|
def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
114
126
|
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
|
|
127
|
+
monkeypatch.setattr(
|
|
128
|
+
check_convergence, "_probe_codex_percent_left", _null_codex_percent_left
|
|
129
|
+
)
|
|
115
130
|
for each_gate_name in _ALL_LEAF_GATE_NAMES:
|
|
116
131
|
monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
|
|
117
132
|
|
|
@@ -122,7 +137,7 @@ def test_all_pass_stdout_and_exit_code_match_the_pinned_contract(
|
|
|
122
137
|
_patch_gates_clean(monkeypatch)
|
|
123
138
|
exit_code = check_convergence.check_all(
|
|
124
139
|
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
125
|
-
)
|
|
140
|
+
, is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
|
|
126
141
|
assert capsys.readouterr().out == EXPECTED_ALL_PASS_STDOUT
|
|
127
142
|
assert exit_code == 0
|
|
128
143
|
|
|
@@ -133,7 +148,7 @@ def test_bugbot_down_bypass_line_matches_the_pinned_contract(
|
|
|
133
148
|
_patch_gates_clean(monkeypatch)
|
|
134
149
|
exit_code = check_convergence.check_all(
|
|
135
150
|
owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
|
|
136
|
-
)
|
|
151
|
+
, is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
|
|
137
152
|
assert capsys.readouterr().out == EXPECTED_BUGBOT_DOWN_STDOUT
|
|
138
153
|
assert exit_code == 0
|
|
139
154
|
|
|
@@ -144,7 +159,7 @@ def test_copilot_down_bypass_lines_match_the_pinned_contract(
|
|
|
144
159
|
_patch_gates_clean(monkeypatch)
|
|
145
160
|
exit_code = check_convergence.check_all(
|
|
146
161
|
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
|
|
147
|
-
)
|
|
162
|
+
, is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
|
|
148
163
|
assert capsys.readouterr().out == EXPECTED_COPILOT_DOWN_STDOUT
|
|
149
164
|
assert exit_code == 0
|
|
150
165
|
|
|
@@ -160,9 +175,9 @@ def test_a_failing_gate_yields_exit_one_and_the_fail_summary(
|
|
|
160
175
|
monkeypatch.setattr(check_convergence, "_get_mergeable", _blocked_mergeable)
|
|
161
176
|
exit_code = check_convergence.check_all(
|
|
162
177
|
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
163
|
-
)
|
|
178
|
+
, is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
|
|
164
179
|
captured_stdout = capsys.readouterr().out
|
|
165
|
-
assert "
|
|
180
|
+
assert "7. PR is mergeable: FAIL — blocked\n" in captured_stdout
|
|
166
181
|
assert captured_stdout.endswith(EXPECTED_FAIL_SUMMARY)
|
|
167
182
|
assert exit_code == 1
|
|
168
183
|
|
|
@@ -177,7 +192,7 @@ def test_gh_error_exit_code_is_two_and_propagates_from_head_fetch(
|
|
|
177
192
|
with pytest.raises(SystemExit) as raised:
|
|
178
193
|
check_convergence.check_all(
|
|
179
194
|
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
180
|
-
)
|
|
195
|
+
, is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
|
|
181
196
|
assert EXIT_CODE_GH_ERROR == 2
|
|
182
197
|
assert raised.value.code == 2
|
|
183
198
|
|
|
@@ -216,6 +231,8 @@ def test_skip_bugteam_gate_when_bugteam_post_blocked_is_true(
|
|
|
216
231
|
is_bugbot_down=False,
|
|
217
232
|
is_copilot_down=False,
|
|
218
233
|
is_bugteam_post_blocked=True,
|
|
234
|
+
is_codex_down=False,
|
|
235
|
+
live_codex_clean_at=None,
|
|
219
236
|
)
|
|
220
237
|
assert "bypassed (bugteam_post_blocked)" in capsys.readouterr().out
|
|
221
238
|
assert exit_code == 0
|
|
@@ -241,6 +258,8 @@ def test_run_bugteam_gate_when_bugteam_post_blocked_is_false(
|
|
|
241
258
|
is_bugbot_down=False,
|
|
242
259
|
is_copilot_down=False,
|
|
243
260
|
is_bugteam_post_blocked=False,
|
|
261
|
+
is_codex_down=False,
|
|
262
|
+
live_codex_clean_at=None,
|
|
244
263
|
)
|
|
245
264
|
assert "_check_bugteam_clean" in invoked_gate_names
|
|
246
265
|
assert "bypassed (bugteam_post_blocked)" not in capsys.readouterr().out
|
|
@@ -311,6 +330,9 @@ def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
|
|
|
311
330
|
is_bugbot_down: bool,
|
|
312
331
|
is_copilot_down: bool,
|
|
313
332
|
is_bugteam_post_blocked: bool = False,
|
|
333
|
+
is_codex_down: bool = False,
|
|
334
|
+
live_codex_clean_at: str | None = None,
|
|
335
|
+
**_bypass_note_kwargs: str,
|
|
314
336
|
) -> int:
|
|
315
337
|
captured_bugteam_post_blocked.append(is_bugteam_post_blocked)
|
|
316
338
|
return 0
|
|
@@ -319,3 +341,53 @@ def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
|
|
|
319
341
|
exit_code = check_convergence.main(["--owner", "o", "--repo", "r", "--pr-number", "1"])
|
|
320
342
|
assert exit_code == 0
|
|
321
343
|
assert captured_bugteam_post_blocked == [True]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def test_copilot_probe_error_still_runs_live_gates(
|
|
347
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
348
|
+
) -> None:
|
|
349
|
+
"""A probe error must not hard-fail; live GitHub gates decide pass/fail."""
|
|
350
|
+
_patch_gates_clean(monkeypatch)
|
|
351
|
+
live_copilot_calls: list[str] = []
|
|
352
|
+
live_pending_calls: list[str] = []
|
|
353
|
+
|
|
354
|
+
def _recording_bot_review(**_keywords: object) -> tuple[bool, str]:
|
|
355
|
+
live_copilot_calls.append("copilot")
|
|
356
|
+
return True, "clean"
|
|
357
|
+
|
|
358
|
+
def _recording_pending(**_keywords: object) -> tuple[bool, str]:
|
|
359
|
+
live_pending_calls.append("pending")
|
|
360
|
+
return True, "clean"
|
|
361
|
+
|
|
362
|
+
monkeypatch.setattr(check_convergence, "_check_bot_review", _recording_bot_review)
|
|
363
|
+
monkeypatch.setattr(
|
|
364
|
+
check_convergence, "_check_no_pending_reviews", _recording_pending
|
|
365
|
+
)
|
|
366
|
+
monkeypatch.setattr(
|
|
367
|
+
check_convergence,
|
|
368
|
+
"_resolve_copilot_waiver",
|
|
369
|
+
lambda _flag: check_convergence_availability.ReviewerWaiver(
|
|
370
|
+
False, "", "quota API down"
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
monkeypatch.setattr(
|
|
374
|
+
check_convergence,
|
|
375
|
+
"_resolve_bugbot_waiver",
|
|
376
|
+
lambda _flag: check_convergence_availability.ReviewerWaiver(True, "bugbot_down", ""),
|
|
377
|
+
)
|
|
378
|
+
exit_code = check_convergence.main(
|
|
379
|
+
[
|
|
380
|
+
"--owner",
|
|
381
|
+
"o",
|
|
382
|
+
"--repo",
|
|
383
|
+
"r",
|
|
384
|
+
"--pr-number",
|
|
385
|
+
"1",
|
|
386
|
+
"--bugbot-down",
|
|
387
|
+
]
|
|
388
|
+
)
|
|
389
|
+
captured = capsys.readouterr().out
|
|
390
|
+
assert live_copilot_calls == ["copilot"]
|
|
391
|
+
assert live_pending_calls == ["pending"]
|
|
392
|
+
assert "enforced (probe error" not in captured
|
|
393
|
+
assert exit_code == 0
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Fixture-mode tests for check_convergence --fixture replay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
import _pr_converge_path_setup # noqa: F401
|
|
14
|
+
|
|
15
|
+
_SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
|
|
16
|
+
_PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
|
|
17
|
+
|
|
18
|
+
CLEAN_LABEL = "Clean \u2014 no findings"
|
|
19
|
+
CLEAN_BODY = (
|
|
20
|
+
f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
|
|
21
|
+
"No findings.\n"
|
|
22
|
+
)
|
|
23
|
+
HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_module() -> ModuleType:
|
|
27
|
+
for each_cached_name in [
|
|
28
|
+
each_key
|
|
29
|
+
for each_key in list(sys.modules)
|
|
30
|
+
if each_key == "config" or each_key.startswith("config.")
|
|
31
|
+
]:
|
|
32
|
+
sys.modules.pop(each_cached_name, None)
|
|
33
|
+
if str(_PR_CONVERGE_DIRECTORY) in sys.path:
|
|
34
|
+
sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
|
|
35
|
+
sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
|
|
36
|
+
module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
|
|
37
|
+
spec = importlib.util.spec_from_file_location(
|
|
38
|
+
"check_convergence_fixture_under_test", module_path
|
|
39
|
+
)
|
|
40
|
+
assert spec is not None
|
|
41
|
+
assert spec.loader is not None
|
|
42
|
+
module = importlib.util.module_from_spec(spec)
|
|
43
|
+
spec.loader.exec_module(module)
|
|
44
|
+
return module
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
check_convergence = _load_module()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _write_passing_fixture(tmp_path: Path) -> Path:
|
|
51
|
+
fixture_path = tmp_path / "pr-at-merge.json"
|
|
52
|
+
payload = {
|
|
53
|
+
"head_sha": HEAD_SHA,
|
|
54
|
+
"pr_object": {"mergeable": True, "mergeable_state": "clean"},
|
|
55
|
+
"reviews": [
|
|
56
|
+
{
|
|
57
|
+
"id": 1,
|
|
58
|
+
"body": CLEAN_BODY,
|
|
59
|
+
"commit_id": HEAD_SHA,
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"unresolved_bot_threads_passed": True,
|
|
63
|
+
"unresolved_bot_threads_detail": "0 unresolved",
|
|
64
|
+
"pending_reviews_passed": True,
|
|
65
|
+
"pending_reviews_detail": "none pending",
|
|
66
|
+
}
|
|
67
|
+
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
68
|
+
return fixture_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def should_exit_zero_from_fixture_with_bugbot_and_copilot_down(
|
|
72
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
73
|
+
) -> None:
|
|
74
|
+
fixture_path = _write_passing_fixture(tmp_path)
|
|
75
|
+
exit_code = check_convergence.main(
|
|
76
|
+
[
|
|
77
|
+
"--owner",
|
|
78
|
+
"jl-cmd",
|
|
79
|
+
"--repo",
|
|
80
|
+
"claude-dev-env",
|
|
81
|
+
"--pr-number",
|
|
82
|
+
"53",
|
|
83
|
+
"--fixture",
|
|
84
|
+
str(fixture_path),
|
|
85
|
+
"--bugbot-down",
|
|
86
|
+
"--copilot-down",
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
captured = capsys.readouterr().out
|
|
90
|
+
assert exit_code == 0
|
|
91
|
+
assert "All pre-conditions met" in captured
|
|
92
|
+
assert "bypassed (bugbot_down)" in captured
|
|
93
|
+
assert "bypassed (copilot_down)" in captured
|
|
94
|
+
assert "clean bugteam audit" in captured
|
|
95
|
+
assert "PR is mergeable: PASS" in captured
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def should_exit_one_from_fixture_when_bugteam_clean_missing(
|
|
99
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
100
|
+
) -> None:
|
|
101
|
+
fixture_path = tmp_path / "missing-bugteam.json"
|
|
102
|
+
payload = {
|
|
103
|
+
"head_sha": HEAD_SHA,
|
|
104
|
+
"pr_object": {"mergeable": True, "mergeable_state": "clean"},
|
|
105
|
+
"reviews": [],
|
|
106
|
+
"unresolved_bot_threads_passed": True,
|
|
107
|
+
"unresolved_bot_threads_detail": "0 unresolved",
|
|
108
|
+
"pending_reviews_passed": True,
|
|
109
|
+
"pending_reviews_detail": "none pending",
|
|
110
|
+
}
|
|
111
|
+
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
112
|
+
exit_code = check_convergence.main(
|
|
113
|
+
[
|
|
114
|
+
"--owner",
|
|
115
|
+
"jl-cmd",
|
|
116
|
+
"--repo",
|
|
117
|
+
"claude-dev-env",
|
|
118
|
+
"--pr-number",
|
|
119
|
+
"80",
|
|
120
|
+
"--fixture",
|
|
121
|
+
str(fixture_path),
|
|
122
|
+
"--bugbot-down",
|
|
123
|
+
"--copilot-down",
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
captured = capsys.readouterr().out
|
|
127
|
+
assert exit_code == 1
|
|
128
|
+
assert "no bugteam review found" in captured
|
|
129
|
+
assert "do not mark ready" in captured
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def should_parse_fixture_flag() -> None:
|
|
133
|
+
arguments = check_convergence.parse_arguments(
|
|
134
|
+
[
|
|
135
|
+
"--owner",
|
|
136
|
+
"o",
|
|
137
|
+
"--repo",
|
|
138
|
+
"r",
|
|
139
|
+
"--pr-number",
|
|
140
|
+
"1",
|
|
141
|
+
"--fixture",
|
|
142
|
+
"snap.json",
|
|
143
|
+
"--bugbot-down",
|
|
144
|
+
"--copilot-down",
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
assert arguments.fixture == "snap.json"
|
|
148
|
+
assert arguments.bugbot_down is True
|
|
149
|
+
assert arguments.copilot_down is True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def should_evaluate_bugteam_clean_from_reviews_on_clean_body() -> None:
|
|
153
|
+
head = HEAD_SHA
|
|
154
|
+
passed, detail = check_convergence._evaluate_bugteam_clean_from_reviews(
|
|
155
|
+
[{"id": 9, "body": CLEAN_BODY, "commit_id": head}],
|
|
156
|
+
head,
|
|
157
|
+
)
|
|
158
|
+
assert passed is True
|
|
159
|
+
assert "clean bugteam audit" in detail
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def should_load_convergence_fixture_from_disk(tmp_path: Path) -> None:
|
|
163
|
+
fixture_path = _write_passing_fixture(tmp_path)
|
|
164
|
+
fixture = check_convergence._load_convergence_fixture(fixture_path)
|
|
165
|
+
assert fixture.head_sha == HEAD_SHA
|
|
166
|
+
assert fixture.pr_object["mergeable"] is True
|
|
167
|
+
assert len(fixture.reviews) == 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def should_reject_fixture_missing_safety_gate_flags(tmp_path: Path) -> None:
|
|
171
|
+
fixture_path = tmp_path / "missing-safety-flags.json"
|
|
172
|
+
payload = {
|
|
173
|
+
"head_sha": HEAD_SHA,
|
|
174
|
+
"pr_object": {"mergeable": True, "mergeable_state": "clean"},
|
|
175
|
+
"reviews": [{"id": 1, "body": CLEAN_BODY, "commit_id": HEAD_SHA}],
|
|
176
|
+
}
|
|
177
|
+
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
178
|
+
with pytest.raises(ValueError):
|
|
179
|
+
check_convergence._load_convergence_fixture(fixture_path)
|
|
@@ -1,68 +1,84 @@
|
|
|
1
|
-
"""Behavioral tests for the review and Bugbot convergence gate leaves.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
_flatten_paginated_reviews(two pages) -> newest-first flat list
|
|
6
|
-
_bugbot_run_conclusion_detail(success) -> (True, "check run ...")
|
|
7
|
-
_check_bugbot(named complete run) -> (True, "check run ...")
|
|
8
|
-
|
|
9
|
-
Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
import check_convergence_gates as gates
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
|
|
22
|
-
pages = [
|
|
23
|
-
[{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
|
|
24
|
-
[{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
|
|
25
|
-
]
|
|
26
|
-
flattened = gates._flatten_paginated_reviews(json.dumps(pages))
|
|
27
|
-
assert flattened is not None
|
|
28
|
-
assert [each_review["id"] for each_review in flattened] == [2, 1]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
|
|
32
|
-
assert (
|
|
33
|
-
gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
|
|
38
|
-
complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
|
|
39
|
-
passed, detail = gates._bugbot_run_conclusion_detail(
|
|
40
|
-
{"id": 55, "conclusion": complete_conclusion, "html_url": ""}
|
|
41
|
-
)
|
|
42
|
-
assert passed is True
|
|
43
|
-
assert "check run 55" in detail
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_check_bugbot_reports_success_when_the_named_run_is_complete(
|
|
47
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
48
|
-
) -> None:
|
|
49
|
-
complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
|
|
50
|
-
run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
|
|
51
|
-
payload = {
|
|
52
|
-
"check_runs": [
|
|
53
|
-
{
|
|
54
|
-
"name": run_name,
|
|
55
|
-
"id": 9,
|
|
56
|
-
"conclusion": complete_conclusion,
|
|
57
|
-
"html_url": "",
|
|
58
|
-
}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
|
|
63
|
-
return 0, json.dumps(payload)
|
|
64
|
-
|
|
65
|
-
monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
|
|
66
|
-
passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
|
|
67
|
-
assert passed is True
|
|
68
|
-
assert "check run 9" in detail
|
|
1
|
+
"""Behavioral tests for the review and Bugbot convergence gate leaves.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
_flatten_paginated_reviews(two pages) -> newest-first flat list
|
|
6
|
+
_bugbot_run_conclusion_detail(success) -> (True, "check run ...")
|
|
7
|
+
_check_bugbot(named complete run) -> (True, "check run ...")
|
|
8
|
+
|
|
9
|
+
Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
import check_convergence_gates as gates
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
|
|
22
|
+
pages = [
|
|
23
|
+
[{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
|
|
24
|
+
[{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
|
|
25
|
+
]
|
|
26
|
+
flattened = gates._flatten_paginated_reviews(json.dumps(pages))
|
|
27
|
+
assert flattened is not None
|
|
28
|
+
assert [each_review["id"] for each_review in flattened] == [2, 1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
|
|
32
|
+
assert (
|
|
33
|
+
gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
|
|
38
|
+
complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
|
|
39
|
+
passed, detail = gates._bugbot_run_conclusion_detail(
|
|
40
|
+
{"id": 55, "conclusion": complete_conclusion, "html_url": ""}
|
|
41
|
+
)
|
|
42
|
+
assert passed is True
|
|
43
|
+
assert "check run 55" in detail
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_check_bugbot_reports_success_when_the_named_run_is_complete(
|
|
47
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
48
|
+
) -> None:
|
|
49
|
+
complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
|
|
50
|
+
run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
|
|
51
|
+
payload = {
|
|
52
|
+
"check_runs": [
|
|
53
|
+
{
|
|
54
|
+
"name": run_name,
|
|
55
|
+
"id": 9,
|
|
56
|
+
"conclusion": complete_conclusion,
|
|
57
|
+
"html_url": "",
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
|
|
63
|
+
return 0, json.dumps(payload)
|
|
64
|
+
|
|
65
|
+
monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
|
|
66
|
+
passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
|
|
67
|
+
assert passed is True
|
|
68
|
+
assert "check run 9" in detail
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def should_evaluate_mergeable_from_pr_object_clean() -> None:
|
|
72
|
+
passed, detail = gates._evaluate_mergeable_from_pr_object(
|
|
73
|
+
{"mergeable": True, "mergeable_state": "clean"}
|
|
74
|
+
)
|
|
75
|
+
assert passed is True
|
|
76
|
+
assert detail == "clean"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def should_evaluate_mergeable_from_pr_object_unknown() -> None:
|
|
80
|
+
passed, detail = gates._evaluate_mergeable_from_pr_object(
|
|
81
|
+
{"mergeable": None, "mergeable_state": "unknown"}
|
|
82
|
+
)
|
|
83
|
+
assert passed is False
|
|
84
|
+
assert detail == "unknown"
|
|
@@ -67,6 +67,30 @@ def test_count_unresolved_bot_threads_counts_a_single_cursor_thread(
|
|
|
67
67
|
assert "src/app.py" in detail
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def test_check_no_pending_reviews_fails_closed_on_unparseable_response(
|
|
71
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
72
|
+
) -> None:
|
|
73
|
+
def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
|
|
74
|
+
return 0, "not json at all"
|
|
75
|
+
|
|
76
|
+
monkeypatch.setattr(thread_gates, "_gh_api", _stub_gh_api)
|
|
77
|
+
passed, detail = thread_gates._check_no_pending_reviews(owner="o", repo="r", number=1)
|
|
78
|
+
assert passed is False
|
|
79
|
+
assert "not valid JSON" in detail
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_check_no_pending_reviews_passes_when_no_copilot_requested(
|
|
83
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
84
|
+
) -> None:
|
|
85
|
+
def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
|
|
86
|
+
return 0, json.dumps({"users": [], "teams": []})
|
|
87
|
+
|
|
88
|
+
monkeypatch.setattr(thread_gates, "_gh_api", _stub_gh_api)
|
|
89
|
+
passed, detail = thread_gates._check_no_pending_reviews(owner="o", repo="r", number=1)
|
|
90
|
+
assert passed is True
|
|
91
|
+
assert "no pending reviewers" in detail
|
|
92
|
+
|
|
93
|
+
|
|
70
94
|
def test_format_unresolved_detail_bounds_the_listed_paths() -> None:
|
|
71
95
|
over_limit = thread_gates.UNRESOLVED_THREAD_DETAIL_MAX + 2
|
|
72
96
|
all_threads = [{"path": f"file{each_index}.py"} for each_index in range(over_limit)]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Contract checks for Step 5 host-aware CODE_REVIEW wiring.
|
|
2
|
+
|
|
3
|
+
Issue #103: Step 5 names ``invoke_code_review.py``, the mode decision inputs
|
|
4
|
+
(host profile, session model), stdin/cwd rules, and the clean/dirty branches
|
|
5
|
+
without changing ``code_review_clean_at`` semantics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SKILL_ROOT = Path(__file__).resolve().parent
|
|
14
|
+
SKILL_MARKDOWN_PATH = SKILL_ROOT / "SKILL.md"
|
|
15
|
+
PER_TICK_MARKDOWN_PATH = SKILL_ROOT / "reference" / "per-tick.md"
|
|
16
|
+
|
|
17
|
+
HELPER_SCRIPT_NAME = "invoke_code_review.py"
|
|
18
|
+
SESSION_MODEL_FLAG = "--session-model"
|
|
19
|
+
HOST_PROFILE_PHRASE = "host profile"
|
|
20
|
+
SESSION_MODEL_PHRASE = "session model"
|
|
21
|
+
IN_SESSION_MODE = "in_session"
|
|
22
|
+
CHAIN_MODE = "chain"
|
|
23
|
+
DIRTY_TREE_KEY = "dirty_tree"
|
|
24
|
+
RETURNCODE_KEY = "returncode"
|
|
25
|
+
SERVED_COMMAND_KEY = "served_command"
|
|
26
|
+
SUCCESSFUL_SERVE_PHRASE = "successful serve"
|
|
27
|
+
FAILED_REVIEW_PHRASE = "Failed review"
|
|
28
|
+
CODE_REVIEW_CLEAN_AT = "code_review_clean_at"
|
|
29
|
+
CLEAN_STAMP_HELPER = "is_code_review_clean_stamp_allowed"
|
|
30
|
+
NEVER_COMMITS_PHRASE = "never commits"
|
|
31
|
+
NEVER_PUSHES_PHRASE = "never pushes"
|
|
32
|
+
EMPTY_STDIN_PHRASE = "empty"
|
|
33
|
+
CWD_FLAG = "--cwd"
|
|
34
|
+
OPUS_MODEL = "opus"
|
|
35
|
+
HIGH_EFFORT_SLASH = "/code-review high --fix"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_markdown(markdown_path: Path) -> str:
|
|
39
|
+
return markdown_path.read_text(encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _code_review_section(per_tick_text: str) -> str:
|
|
43
|
+
section_start = per_tick_text.index("### `phase == CODE_REVIEW`")
|
|
44
|
+
section_end = per_tick_text.index("### `phase == BUGTEAM`", section_start)
|
|
45
|
+
return per_tick_text[section_start:section_end]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _step_five_checklist(skill_text: str) -> str:
|
|
49
|
+
step_start = skill_text.index("**Step 5: CODE-REVIEW")
|
|
50
|
+
step_end = skill_text.index("**Step 6: BUGTEAM", step_start)
|
|
51
|
+
return skill_text[step_start:step_end]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_per_tick_code_review_names_helper_and_mode_inputs() -> None:
|
|
55
|
+
code_review_section = _code_review_section(_read_markdown(PER_TICK_MARKDOWN_PATH))
|
|
56
|
+
assert HELPER_SCRIPT_NAME in code_review_section
|
|
57
|
+
assert HOST_PROFILE_PHRASE in code_review_section
|
|
58
|
+
assert SESSION_MODEL_PHRASE in code_review_section
|
|
59
|
+
assert SESSION_MODEL_FLAG in code_review_section
|
|
60
|
+
assert CWD_FLAG in code_review_section
|
|
61
|
+
assert IN_SESSION_MODE in code_review_section
|
|
62
|
+
assert CHAIN_MODE in code_review_section
|
|
63
|
+
assert DIRTY_TREE_KEY in code_review_section
|
|
64
|
+
assert RETURNCODE_KEY in code_review_section
|
|
65
|
+
assert SERVED_COMMAND_KEY in code_review_section
|
|
66
|
+
assert SUCCESSFUL_SERVE_PHRASE in code_review_section
|
|
67
|
+
assert FAILED_REVIEW_PHRASE in code_review_section
|
|
68
|
+
assert CLEAN_STAMP_HELPER in code_review_section
|
|
69
|
+
assert HIGH_EFFORT_SLASH in code_review_section
|
|
70
|
+
assert OPUS_MODEL in code_review_section
|
|
71
|
+
assert NEVER_COMMITS_PHRASE in code_review_section
|
|
72
|
+
assert NEVER_PUSHES_PHRASE in code_review_section
|
|
73
|
+
assert EMPTY_STDIN_PHRASE in code_review_section
|
|
74
|
+
assert CODE_REVIEW_CLEAN_AT in code_review_section
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_skill_step_five_checklist_names_helper_and_mode_inputs() -> None:
|
|
78
|
+
step_five_checklist = _step_five_checklist(_read_markdown(SKILL_MARKDOWN_PATH))
|
|
79
|
+
assert HELPER_SCRIPT_NAME in step_five_checklist
|
|
80
|
+
assert HOST_PROFILE_PHRASE in step_five_checklist
|
|
81
|
+
assert SESSION_MODEL_PHRASE in step_five_checklist
|
|
82
|
+
assert SESSION_MODEL_FLAG in step_five_checklist
|
|
83
|
+
assert CWD_FLAG in step_five_checklist
|
|
84
|
+
assert IN_SESSION_MODE in step_five_checklist
|
|
85
|
+
assert CHAIN_MODE in step_five_checklist
|
|
86
|
+
assert DIRTY_TREE_KEY in step_five_checklist
|
|
87
|
+
assert RETURNCODE_KEY in step_five_checklist
|
|
88
|
+
assert SERVED_COMMAND_KEY in step_five_checklist
|
|
89
|
+
assert SUCCESSFUL_SERVE_PHRASE in step_five_checklist
|
|
90
|
+
assert HIGH_EFFORT_SLASH in step_five_checklist
|
|
91
|
+
assert OPUS_MODEL in step_five_checklist
|
|
92
|
+
assert NEVER_COMMITS_PHRASE in step_five_checklist
|
|
93
|
+
assert NEVER_PUSHES_PHRASE in step_five_checklist
|
|
94
|
+
assert CODE_REVIEW_CLEAN_AT in step_five_checklist
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_per_tick_clean_stamp_requires_successful_serve() -> None:
|
|
98
|
+
code_review_section = _code_review_section(_read_markdown(PER_TICK_MARKDOWN_PATH))
|
|
99
|
+
assert FAILED_REVIEW_PHRASE in code_review_section
|
|
100
|
+
assert "returncode != 0" in code_review_section
|
|
101
|
+
assert "null" in code_review_section
|
|
102
|
+
assert SERVED_COMMAND_KEY in code_review_section
|
|
103
|
+
assert "do not set" in code_review_section.lower() or "Do not set" in code_review_section
|
|
104
|
+
assert CODE_REVIEW_CLEAN_AT in code_review_section
|
|
105
|
+
assert SUCCESSFUL_SERVE_PHRASE in code_review_section
|
|
106
|
+
assert DIRTY_TREE_KEY in code_review_section
|