claude-dev-env 1.92.1 → 1.93.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +260 -14
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +292 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +365 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +190 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +295 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Decide which PR reviewers run from the opt-out and opt-in env token lists.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
enabled lists bugbot, disabled empty -> bugbot ok: runs
|
|
6
|
+
no lists set -> bugbot flag: off (default)
|
|
7
|
+
enabled lists bugbot, disabled bugbot -> bugbot flag: off (opt-out wins)
|
|
8
|
+
disabled empty -> bugteam ok: runs
|
|
9
|
+
disabled lists copilot -> copilot flag: off
|
|
10
|
+
|
|
11
|
+
Bugbot is off by default and runs only when the enabled list names it.
|
|
12
|
+
Bugteam and copilot run by default and stop only when the disabled list
|
|
13
|
+
names them; both lists parse case-insensitively and tolerate whitespace.
|
|
6
14
|
"""
|
|
7
15
|
|
|
8
16
|
from __future__ import annotations
|
|
@@ -17,6 +25,7 @@ from pr_loop_shared_constants.reviews_disabled_constants import (
|
|
|
17
25
|
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
18
26
|
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
|
|
19
27
|
CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
|
|
28
|
+
CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME,
|
|
20
29
|
EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV,
|
|
21
30
|
)
|
|
22
31
|
|
|
@@ -27,18 +36,24 @@ __all__ = [
|
|
|
27
36
|
"CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN",
|
|
28
37
|
"CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME",
|
|
29
38
|
"CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR",
|
|
39
|
+
"CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME",
|
|
30
40
|
"EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV",
|
|
31
41
|
"is_bugbot_disabled_via_env",
|
|
42
|
+
"is_bugbot_opted_out_via_env",
|
|
32
43
|
"is_bugteam_disabled_via_env",
|
|
33
44
|
"is_copilot_disabled_via_env",
|
|
34
45
|
"main",
|
|
35
46
|
]
|
|
36
47
|
|
|
37
48
|
|
|
38
|
-
def
|
|
39
|
-
|
|
49
|
+
def _is_reviewer_listed_in_env(
|
|
50
|
+
environment_variable_name: str, reviewer_token: str
|
|
51
|
+
) -> bool:
|
|
52
|
+
"""Check whether an environment variable lists the given reviewer token.
|
|
40
53
|
|
|
41
54
|
Args:
|
|
55
|
+
environment_variable_name: The environment variable to read, either
|
|
56
|
+
the reviews-disabled or the reviews-enabled variable name.
|
|
42
57
|
reviewer_token: The reviewer token to look for, already lowercase
|
|
43
58
|
(for example the bugteam or bugbot token constant).
|
|
44
59
|
|
|
@@ -46,16 +61,14 @@ def _is_reviewer_disabled_via_env(reviewer_token: str) -> bool:
|
|
|
46
61
|
True when the env var contains ``reviewer_token`` as one of its
|
|
47
62
|
comma-separated entries (case-insensitive, whitespace-tolerant).
|
|
48
63
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
reviews_token_separator = CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR
|
|
65
|
+
listed_reviewers_text = os.environ.get(environment_variable_name, "")
|
|
66
|
+
all_listed_tokens = frozenset(
|
|
52
67
|
each_raw_token.strip().lower()
|
|
53
|
-
for each_raw_token in
|
|
54
|
-
reviews_disabled_token_separator
|
|
55
|
-
)
|
|
68
|
+
for each_raw_token in listed_reviewers_text.split(reviews_token_separator)
|
|
56
69
|
if each_raw_token.strip()
|
|
57
70
|
)
|
|
58
|
-
return reviewer_token in
|
|
71
|
+
return reviewer_token in all_listed_tokens
|
|
59
72
|
|
|
60
73
|
|
|
61
74
|
def is_bugteam_disabled_via_env() -> bool:
|
|
@@ -64,16 +77,45 @@ def is_bugteam_disabled_via_env() -> bool:
|
|
|
64
77
|
Returns:
|
|
65
78
|
True when the env var lists the ``bugteam`` token.
|
|
66
79
|
"""
|
|
67
|
-
return
|
|
80
|
+
return _is_reviewer_listed_in_env(
|
|
81
|
+
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN
|
|
82
|
+
)
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
def is_bugbot_disabled_via_env() -> bool:
|
|
86
|
+
"""Check whether Cursor Bugbot is disabled for this run.
|
|
87
|
+
|
|
88
|
+
Cursor Bugbot is off by default. It runs only when
|
|
89
|
+
``CLAUDE_REVIEWS_ENABLED`` lists ``bugbot``, and a ``bugbot`` token in
|
|
90
|
+
``CLAUDE_REVIEWS_DISABLED`` forces it off even when the opt-in lists it.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True when ``CLAUDE_REVIEWS_DISABLED`` lists ``bugbot`` or when
|
|
94
|
+
``CLAUDE_REVIEWS_ENABLED`` does not list ``bugbot``.
|
|
95
|
+
"""
|
|
96
|
+
is_opted_out = _is_reviewer_listed_in_env(
|
|
97
|
+
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN
|
|
98
|
+
)
|
|
99
|
+
is_opted_in = _is_reviewer_listed_in_env(
|
|
100
|
+
CLAUDE_REVIEWS_ENABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN
|
|
101
|
+
)
|
|
102
|
+
return is_opted_out or not is_opted_in
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_bugbot_opted_out_via_env() -> bool:
|
|
71
106
|
"""Check whether CLAUDE_REVIEWS_DISABLED opts Cursor Bugbot out.
|
|
72
107
|
|
|
108
|
+
The opt-out forces Bugbot off even when ``CLAUDE_REVIEWS_ENABLED`` lists
|
|
109
|
+
``bugbot``. This reports only the opt-out signal, where
|
|
110
|
+
``is_bugbot_disabled_via_env`` also reports off for the default case in
|
|
111
|
+
which neither env var names ``bugbot``.
|
|
112
|
+
|
|
73
113
|
Returns:
|
|
74
|
-
True when
|
|
114
|
+
True when ``CLAUDE_REVIEWS_DISABLED`` lists the ``bugbot`` token.
|
|
75
115
|
"""
|
|
76
|
-
return
|
|
116
|
+
return _is_reviewer_listed_in_env(
|
|
117
|
+
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN
|
|
118
|
+
)
|
|
77
119
|
|
|
78
120
|
|
|
79
121
|
def is_copilot_disabled_via_env() -> bool:
|
|
@@ -82,11 +124,13 @@ def is_copilot_disabled_via_env() -> bool:
|
|
|
82
124
|
Returns:
|
|
83
125
|
True when the env var lists the ``copilot`` token.
|
|
84
126
|
"""
|
|
85
|
-
return
|
|
127
|
+
return _is_reviewer_listed_in_env(
|
|
128
|
+
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN
|
|
129
|
+
)
|
|
86
130
|
|
|
87
131
|
|
|
88
132
|
def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
89
|
-
"""Parse command-line arguments for the reviewer opt-out check.
|
|
133
|
+
"""Parse command-line arguments for the reviewer opt-out and opt-in gate check.
|
|
90
134
|
|
|
91
135
|
Args:
|
|
92
136
|
all_argv: Argument list excluding the program name, typically
|
|
@@ -105,19 +149,19 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
105
149
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
|
|
106
150
|
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
107
151
|
],
|
|
108
|
-
help="Reviewer token to test against CLAUDE_REVIEWS_DISABLED",
|
|
152
|
+
help="Reviewer token to test against the CLAUDE_REVIEWS_DISABLED / CLAUDE_REVIEWS_ENABLED gates",
|
|
109
153
|
)
|
|
110
154
|
return parser.parse_args(all_argv)
|
|
111
155
|
|
|
112
156
|
|
|
113
157
|
def main(all_arguments: list[str]) -> int:
|
|
114
|
-
"""Exit 0 when the named reviewer is disabled
|
|
158
|
+
"""Exit 0 when the named reviewer is disabled for this run.
|
|
115
159
|
|
|
116
160
|
Args:
|
|
117
161
|
all_arguments: Argument list excluding the program name.
|
|
118
162
|
|
|
119
163
|
Returns:
|
|
120
|
-
0 when the named reviewer is
|
|
164
|
+
0 when the named reviewer is disabled for this run, 1 otherwise.
|
|
121
165
|
"""
|
|
122
166
|
arguments = parse_arguments(all_arguments)
|
|
123
167
|
disabled_checker_by_reviewer = {
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
"""Flag a prose term that names a code identifier a hair differently.
|
|
2
2
|
|
|
3
|
-
A change adds the field ``
|
|
4
|
-
call it the ``premium-request``
|
|
5
|
-
searches one never finds the other.
|
|
3
|
+
A change adds the field ``premium_request_interactions`` while the same
|
|
4
|
+
branch's docs call it the ``premium-request-budget`` field: one thing, two
|
|
5
|
+
names, and a reader who searches one never finds the other.
|
|
6
6
|
|
|
7
7
|
::
|
|
8
8
|
|
|
9
|
-
code adds:
|
|
10
|
-
prose adds: the premium-request
|
|
11
|
-
flag: premium-request vs
|
|
12
|
-
ok: premium-interactions -- exact hyphen form, agrees
|
|
13
|
-
ok:
|
|
9
|
+
code adds: premium_request_interactions the identifier
|
|
10
|
+
prose adds: the premium-request-budget field a hyphenated term
|
|
11
|
+
flag: premium-request-budget vs premium_request_interactions -- only the final word differs
|
|
12
|
+
ok: premium-request-interactions -- exact hyphen form, agrees
|
|
13
|
+
ok: premium-report-total vs premium_request_interactions -- an earlier word differs, ordinary prose
|
|
14
14
|
|
|
15
15
|
The sweep reads a unified diff, collects the multi-word identifiers added on
|
|
16
16
|
code lines, and scans each added prose line for a hyphen or space variant that
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
renames only the final word of an identifier: it agrees on every earlier word
|
|
18
|
+
and differs on the last. A term that diverges in any earlier word, or shares
|
|
19
|
+
only one leading word, is ordinary prose. It also spares English compound
|
|
20
|
+
tails, singular/plural forms, stopword windows, and words that are themselves
|
|
21
|
+
tokens of another introduced identifier.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
import argparse
|
|
@@ -345,6 +347,47 @@ def _tuples_match_ignoring_plural(
|
|
|
345
347
|
)
|
|
346
348
|
|
|
347
349
|
|
|
350
|
+
def _diverges_only_in_final_token(
|
|
351
|
+
candidate_tuple: IdentifierTuple, identifier_tuple: IdentifierTuple
|
|
352
|
+
) -> bool:
|
|
353
|
+
"""Return whether two equal-length tuples agree on every token but the last.
|
|
354
|
+
|
|
355
|
+
::
|
|
356
|
+
|
|
357
|
+
candidate: ("premium", "request", "budget")
|
|
358
|
+
identifier: ("premium", "request", "interactions")
|
|
359
|
+
flag: every earlier token agrees, so the prose renames the final word
|
|
360
|
+
ok: ("premium", "report", "total") -- an earlier token diverges too
|
|
361
|
+
|
|
362
|
+
A near-miss renames a single trailing word of an identifier, so every token
|
|
363
|
+
before the last agrees by an exact or singular/plural match while the final
|
|
364
|
+
token differs. A tuple no longer than the two-token identifier minimum
|
|
365
|
+
cannot carry two agreeing leading tokens, so it never qualifies.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
candidate_tuple: The prose term's lowercase token tuple.
|
|
369
|
+
identifier_tuple: An introduced identifier's lowercase token tuple.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
True when both tuples run longer than the two-token identifier minimum,
|
|
373
|
+
share the same length, agree by exact or singular/plural match on every
|
|
374
|
+
token but the last, and differ on the final token.
|
|
375
|
+
"""
|
|
376
|
+
if (
|
|
377
|
+
len(candidate_tuple) <= MINIMUM_IDENTIFIER_TOKEN_COUNT
|
|
378
|
+
or len(candidate_tuple) != len(identifier_tuple)
|
|
379
|
+
):
|
|
380
|
+
return False
|
|
381
|
+
if _tokens_are_plural_variants(candidate_tuple[-1], identifier_tuple[-1]):
|
|
382
|
+
return False
|
|
383
|
+
return all(
|
|
384
|
+
_tokens_are_plural_variants(each_candidate_token, each_identifier_token)
|
|
385
|
+
for each_candidate_token, each_identifier_token in zip(
|
|
386
|
+
candidate_tuple[:-1], identifier_tuple[:-1]
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
348
391
|
def _word_is_identifier_vocabulary(
|
|
349
392
|
prose_word: str, all_identifier_tokens: frozenset[str]
|
|
350
393
|
) -> bool:
|
|
@@ -391,16 +434,19 @@ def _near_miss_identifier(
|
|
|
391
434
|
lines, used to spare prose built from real code vocabulary.
|
|
392
435
|
|
|
393
436
|
Returns:
|
|
394
|
-
The first identifier
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
(``
|
|
402
|
-
|
|
403
|
-
|
|
437
|
+
The first identifier the candidate renames in only its final token. Such a
|
|
438
|
+
candidate agrees on every earlier token, matches the identifier's
|
|
439
|
+
length, and differs on the last token. None marks the candidate
|
|
440
|
+
ordinary prose instead, on any of these grounds: it matches an
|
|
441
|
+
identifier exactly; it diverges in a token before the last
|
|
442
|
+
(``premium report total`` against ``premium_request_interactions``);
|
|
443
|
+
it holds no more than the two-token identifier minimum; it ends in a
|
|
444
|
+
common English compound tail word (``read-only``, ``data-driven``); it
|
|
445
|
+
contains an English stopword (``to a``, ``each image``); it differs
|
|
446
|
+
from the identifier only by a singular/plural form of one or more
|
|
447
|
+
tokens (``test files`` against ``test_file``); or every diverging word
|
|
448
|
+
is itself a token of some introduced identifier (``target width box``
|
|
449
|
+
when ``box_height`` is also in the diff).
|
|
404
450
|
"""
|
|
405
451
|
if not candidate_tuple or candidate_tuple in all_identifier_tuples:
|
|
406
452
|
return None
|
|
@@ -415,6 +461,8 @@ def _near_miss_identifier(
|
|
|
415
461
|
continue
|
|
416
462
|
if _tuples_match_ignoring_plural(each_identifier, candidate_tuple):
|
|
417
463
|
continue
|
|
464
|
+
if not _diverges_only_in_final_token(candidate_tuple, each_identifier):
|
|
465
|
+
continue
|
|
418
466
|
diverging_words = [
|
|
419
467
|
each_word
|
|
420
468
|
for each_word, each_token in zip(candidate_tuple, each_identifier)
|
|
@@ -32,7 +32,7 @@ pytest suite for the scripts and constants in `_shared/pr-loop/scripts/`. Each t
|
|
|
32
32
|
|
|
33
33
|
## Fixtures
|
|
34
34
|
|
|
35
|
-
`fixtures/
|
|
35
|
+
`fixtures/copilot_internal_user_example.json` — a captured `gh api
|
|
36
36
|
copilot_internal/user` response driving `test_copilot_quota.py`.
|
|
37
37
|
|
|
38
38
|
## Running
|
|
@@ -336,7 +336,7 @@ def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
|
|
|
336
336
|
short_project_path = "/projects/foo"
|
|
337
337
|
trust_prefix = "Trusted local workspace:"
|
|
338
338
|
longer_unrelated_path_entry = (
|
|
339
|
-
"Trusted local workspace: /Users/
|
|
339
|
+
"Trusted local workspace: /Users/example/projects/foo/.claude/** unrelated path"
|
|
340
340
|
)
|
|
341
341
|
assert (
|
|
342
342
|
common_module.is_trust_entry_for_project(
|
|
@@ -337,11 +337,11 @@ def test_main_staged_mode_passes_when_only_terminology_findings_exist(
|
|
|
337
337
|
commit_all_files(temporary_git_repository, "initial")
|
|
338
338
|
write_file(
|
|
339
339
|
temporary_git_repository / "quota.py",
|
|
340
|
-
"first_count = 1\
|
|
340
|
+
"first_count = 1\npremium_request_interactions = 1\n",
|
|
341
341
|
)
|
|
342
342
|
write_file(
|
|
343
343
|
temporary_git_repository / "README.md",
|
|
344
|
-
"The premium-request
|
|
344
|
+
"The premium-request-budget field gates the run.\n",
|
|
345
345
|
)
|
|
346
346
|
stage_file(temporary_git_repository, "quota.py")
|
|
347
347
|
stage_file(temporary_git_repository, "README.md")
|
|
@@ -2043,6 +2043,88 @@ def test_run_staged_test_files_zero_when_staged_conftest_collects_no_tests(
|
|
|
2043
2043
|
assert gate_module.run_staged_test_files(temporary_git_repository) == 0
|
|
2044
2044
|
|
|
2045
2045
|
|
|
2046
|
+
def test_staged_pytest_skipped_below_minimum_python(
|
|
2047
|
+
temporary_git_repository: Path,
|
|
2048
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2049
|
+
capsys: pytest.CaptureFixture[str],
|
|
2050
|
+
) -> None:
|
|
2051
|
+
"""Below the minimum Python, the staged pytest step is skipped, not run.
|
|
2052
|
+
|
|
2053
|
+
A staged test that would fail returns a zero exit code because the older
|
|
2054
|
+
interpreter cannot run its newer-API calls, so the environment gap does not
|
|
2055
|
+
block the commit.
|
|
2056
|
+
"""
|
|
2057
|
+
write_file(
|
|
2058
|
+
temporary_git_repository / "test_needs_new_python.py",
|
|
2059
|
+
"def test_intentionally_fails() -> None:\n assert False\n",
|
|
2060
|
+
)
|
|
2061
|
+
stage_file(temporary_git_repository, "test_needs_new_python.py")
|
|
2062
|
+
monkeypatch.setattr(gate_module, "MINIMUM_STAGED_PYTEST_PYTHON_MAJOR", 99)
|
|
2063
|
+
|
|
2064
|
+
exit_code = gate_module._staged_pytest_exit_code_for_current_python(
|
|
2065
|
+
temporary_git_repository
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
captured = capsys.readouterr()
|
|
2069
|
+
assert exit_code == 0
|
|
2070
|
+
assert "skipping the staged pytest step" in captured.err
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
def test_staged_pytest_runs_at_or_above_minimum_python(
|
|
2074
|
+
temporary_git_repository: Path,
|
|
2075
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2076
|
+
) -> None:
|
|
2077
|
+
"""At or above the minimum Python, the staged pytest step runs and a failing
|
|
2078
|
+
staged test blocks with a non-zero exit code.
|
|
2079
|
+
"""
|
|
2080
|
+
write_file(
|
|
2081
|
+
temporary_git_repository / "test_fails.py",
|
|
2082
|
+
"def test_intentionally_fails() -> None:\n assert False\n",
|
|
2083
|
+
)
|
|
2084
|
+
stage_file(temporary_git_repository, "test_fails.py")
|
|
2085
|
+
monkeypatch.setattr(gate_module, "MINIMUM_STAGED_PYTEST_PYTHON_MAJOR", 2)
|
|
2086
|
+
|
|
2087
|
+
exit_code = gate_module._staged_pytest_exit_code_for_current_python(
|
|
2088
|
+
temporary_git_repository
|
|
2089
|
+
)
|
|
2090
|
+
|
|
2091
|
+
assert exit_code != 0
|
|
2092
|
+
|
|
2093
|
+
|
|
2094
|
+
def test_main_staged_skips_pytest_but_runs_code_rules_below_minimum_python(
|
|
2095
|
+
temporary_git_repository: Path,
|
|
2096
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2097
|
+
capsys: pytest.CaptureFixture[str],
|
|
2098
|
+
) -> None:
|
|
2099
|
+
"""The version guard skips only the staged pytest step. A staged code-rules
|
|
2100
|
+
violation still blocks the commit, so the two checks stay independent.
|
|
2101
|
+
"""
|
|
2102
|
+
write_file(temporary_git_repository / "module.py", "first_count = 1\n")
|
|
2103
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
2104
|
+
write_file(
|
|
2105
|
+
temporary_git_repository / "module.py",
|
|
2106
|
+
"first_count = 1\n"
|
|
2107
|
+
"def compute_total(operand: int) -> int:\n"
|
|
2108
|
+
" result = operand + 1\n"
|
|
2109
|
+
" return result\n",
|
|
2110
|
+
)
|
|
2111
|
+
write_file(
|
|
2112
|
+
temporary_git_repository / "test_fails.py",
|
|
2113
|
+
"def test_intentionally_fails() -> None:\n assert False\n",
|
|
2114
|
+
)
|
|
2115
|
+
stage_file(temporary_git_repository, "module.py")
|
|
2116
|
+
stage_file(temporary_git_repository, "test_fails.py")
|
|
2117
|
+
monkeypatch.setattr(gate_module, "MINIMUM_STAGED_PYTEST_PYTHON_MAJOR", 99)
|
|
2118
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
2119
|
+
|
|
2120
|
+
exit_code = gate_module.main(["--staged"])
|
|
2121
|
+
|
|
2122
|
+
captured = capsys.readouterr()
|
|
2123
|
+
assert exit_code == 1
|
|
2124
|
+
assert "skipping the staged pytest step" in captured.err
|
|
2125
|
+
assert "result" in captured.err
|
|
2126
|
+
|
|
2127
|
+
|
|
2046
2128
|
def test_hunk_header_pattern_captures_new_start_and_count() -> None:
|
|
2047
2129
|
header_match = gate_module.hunk_header_pattern().match("@@ -12,3 +45,6 @@ def scope")
|
|
2048
2130
|
assert header_match is not None
|
|
@@ -2578,3 +2660,211 @@ def test_run_staged_test_files_fails_when_one_group_fails(
|
|
|
2578
2660
|
assert exit_code != 0
|
|
2579
2661
|
captured_error = capsys.readouterr().err
|
|
2580
2662
|
assert "failing_skill" in captured_error
|
|
2663
|
+
|
|
2664
|
+
|
|
2665
|
+
def test_resolve_gate_python_executable_prefers_env_var_over_venv(
|
|
2666
|
+
tmp_path: Path,
|
|
2667
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2668
|
+
) -> None:
|
|
2669
|
+
relative_segments = (
|
|
2670
|
+
gate_module.ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
|
|
2671
|
+
if os.name == "nt"
|
|
2672
|
+
else gate_module.ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
|
|
2673
|
+
)
|
|
2674
|
+
venv_python = tmp_path.joinpath(".venv", *relative_segments)
|
|
2675
|
+
venv_python.parent.mkdir(parents=True)
|
|
2676
|
+
venv_python.write_text("", encoding="utf-8")
|
|
2677
|
+
monkeypatch.setenv("CODE_RULES_GATE_PYTHON", "/configured/python")
|
|
2678
|
+
|
|
2679
|
+
resolved = gate_module._resolve_gate_python_executable(tmp_path)
|
|
2680
|
+
|
|
2681
|
+
assert resolved == "/configured/python"
|
|
2682
|
+
|
|
2683
|
+
|
|
2684
|
+
def test_resolve_gate_python_executable_falls_back_to_project_venv(
|
|
2685
|
+
tmp_path: Path,
|
|
2686
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2687
|
+
) -> None:
|
|
2688
|
+
monkeypatch.delenv("CODE_RULES_GATE_PYTHON", raising=False)
|
|
2689
|
+
relative_segments = (
|
|
2690
|
+
gate_module.ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
|
|
2691
|
+
if os.name == "nt"
|
|
2692
|
+
else gate_module.ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS
|
|
2693
|
+
)
|
|
2694
|
+
venv_python = tmp_path.joinpath(".venv", *relative_segments)
|
|
2695
|
+
venv_python.parent.mkdir(parents=True)
|
|
2696
|
+
venv_python.write_text("", encoding="utf-8")
|
|
2697
|
+
|
|
2698
|
+
resolved = gate_module._resolve_gate_python_executable(tmp_path)
|
|
2699
|
+
|
|
2700
|
+
assert resolved == str(venv_python)
|
|
2701
|
+
|
|
2702
|
+
|
|
2703
|
+
def test_resolve_gate_python_executable_falls_back_to_sys_executable(
|
|
2704
|
+
tmp_path: Path,
|
|
2705
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2706
|
+
) -> None:
|
|
2707
|
+
monkeypatch.delenv("CODE_RULES_GATE_PYTHON", raising=False)
|
|
2708
|
+
|
|
2709
|
+
resolved = gate_module._resolve_gate_python_executable(tmp_path)
|
|
2710
|
+
|
|
2711
|
+
assert resolved == sys.executable
|
|
2712
|
+
|
|
2713
|
+
|
|
2714
|
+
def test_staged_pytest_environment_prepends_configured_pythonpath(
|
|
2715
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2716
|
+
) -> None:
|
|
2717
|
+
monkeypatch.setenv("CODE_RULES_GATE_PYTHONPATH", "/configured/site-packages")
|
|
2718
|
+
monkeypatch.setenv("PYTHONPATH", "/already/on/path")
|
|
2719
|
+
|
|
2720
|
+
environment = gate_module._staged_pytest_environment()
|
|
2721
|
+
|
|
2722
|
+
assert environment["PYTHONPATH"] == os.pathsep.join(
|
|
2723
|
+
["/configured/site-packages", "/already/on/path"]
|
|
2724
|
+
)
|
|
2725
|
+
|
|
2726
|
+
|
|
2727
|
+
def _record_pytest_invocations(
|
|
2728
|
+
monkeypatch: pytest.MonkeyPatch, *, all_exit_codes: list[int]
|
|
2729
|
+
) -> list[list[str]]:
|
|
2730
|
+
"""Patch subprocess.run to record each pytest argv and return staged exit codes."""
|
|
2731
|
+
all_recorded_commands: list[list[str]] = []
|
|
2732
|
+
all_remaining_exit_codes = list(all_exit_codes)
|
|
2733
|
+
|
|
2734
|
+
def _fake_subprocess_run(
|
|
2735
|
+
all_command: list[str], **_keyword_arguments: object
|
|
2736
|
+
) -> subprocess.CompletedProcess[bytes]:
|
|
2737
|
+
all_recorded_commands.append(list(all_command))
|
|
2738
|
+
exit_code = all_remaining_exit_codes.pop(0) if all_remaining_exit_codes else 0
|
|
2739
|
+
return subprocess.CompletedProcess(all_command, exit_code, b"", b"")
|
|
2740
|
+
|
|
2741
|
+
monkeypatch.setattr(gate_module.subprocess, "run", _fake_subprocess_run)
|
|
2742
|
+
return all_recorded_commands
|
|
2743
|
+
|
|
2744
|
+
|
|
2745
|
+
def _staged_test_paths_under(group_root: Path, path_count: int) -> list[Path]:
|
|
2746
|
+
return [
|
|
2747
|
+
group_root
|
|
2748
|
+
/ "tests"
|
|
2749
|
+
/ "subpackage_with_a_realistically_long_directory_name"
|
|
2750
|
+
/ f"test_module_with_a_realistically_long_name_{each_index:04d}.py"
|
|
2751
|
+
for each_index in range(path_count)
|
|
2752
|
+
]
|
|
2753
|
+
|
|
2754
|
+
|
|
2755
|
+
def _path_arguments_of(all_pytest_command: list[str]) -> list[str]:
|
|
2756
|
+
fixed_prefix_length = len(gate_module.ALL_PYTEST_MODULE_INVOCATION) + 1
|
|
2757
|
+
return all_pytest_command[fixed_prefix_length:]
|
|
2758
|
+
|
|
2759
|
+
|
|
2760
|
+
def test_run_pytest_for_group_batches_a_large_staged_set_under_the_budget(
|
|
2761
|
+
tmp_path: Path,
|
|
2762
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2763
|
+
) -> None:
|
|
2764
|
+
"""WinError 206: a staged set whose joined argv passes Windows' 32767-character
|
|
2765
|
+
command-line limit crashed the gate and denied the commit. The paths must be
|
|
2766
|
+
split across several pytest invocations, none of them over the budget.
|
|
2767
|
+
"""
|
|
2768
|
+
all_recorded_commands = _record_pytest_invocations(monkeypatch, all_exit_codes=[])
|
|
2769
|
+
all_test_paths = _staged_test_paths_under(tmp_path, 800)
|
|
2770
|
+
|
|
2771
|
+
exit_code = gate_module._run_pytest_for_group(tmp_path, all_test_paths, tmp_path)
|
|
2772
|
+
|
|
2773
|
+
assert exit_code == 0
|
|
2774
|
+
assert len(all_recorded_commands) > 1, (
|
|
2775
|
+
"a staged set wider than the command-line budget must be split across "
|
|
2776
|
+
"several pytest invocations rather than passed in one oversized argv"
|
|
2777
|
+
)
|
|
2778
|
+
for each_command in all_recorded_commands:
|
|
2779
|
+
assert (
|
|
2780
|
+
len(" ".join(each_command))
|
|
2781
|
+
<= gate_module.MAXIMUM_STAGED_PYTEST_COMMAND_LINE_CHARACTERS
|
|
2782
|
+
)
|
|
2783
|
+
all_batched_arguments = [
|
|
2784
|
+
each_argument
|
|
2785
|
+
for each_command in all_recorded_commands
|
|
2786
|
+
for each_argument in _path_arguments_of(each_command)
|
|
2787
|
+
]
|
|
2788
|
+
assert len(all_batched_arguments) == len(all_test_paths)
|
|
2789
|
+
|
|
2790
|
+
|
|
2791
|
+
def test_run_pytest_for_group_passes_paths_relative_to_the_group_root(
|
|
2792
|
+
tmp_path: Path,
|
|
2793
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2794
|
+
) -> None:
|
|
2795
|
+
"""group_root is already the subprocess cwd, so each path argument is passed
|
|
2796
|
+
relative to it - the shortening that keeps a large staged set under the limit.
|
|
2797
|
+
"""
|
|
2798
|
+
all_recorded_commands = _record_pytest_invocations(monkeypatch, all_exit_codes=[])
|
|
2799
|
+
test_path = tmp_path / "tests" / "test_relative.py"
|
|
2800
|
+
|
|
2801
|
+
gate_module._run_pytest_for_group(tmp_path, [test_path], tmp_path)
|
|
2802
|
+
|
|
2803
|
+
assert _path_arguments_of(all_recorded_commands[0]) == [
|
|
2804
|
+
str(Path("tests") / "test_relative.py")
|
|
2805
|
+
]
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
def test_run_pytest_for_group_passes_an_outside_path_absolute(
|
|
2809
|
+
tmp_path: Path,
|
|
2810
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2811
|
+
) -> None:
|
|
2812
|
+
all_recorded_commands = _record_pytest_invocations(monkeypatch, all_exit_codes=[])
|
|
2813
|
+
group_root = tmp_path / "group"
|
|
2814
|
+
outside_path = tmp_path / "elsewhere" / "test_outside.py"
|
|
2815
|
+
|
|
2816
|
+
gate_module._run_pytest_for_group(group_root, [outside_path], tmp_path)
|
|
2817
|
+
|
|
2818
|
+
assert _path_arguments_of(all_recorded_commands[0]) == [str(outside_path)]
|
|
2819
|
+
|
|
2820
|
+
|
|
2821
|
+
def test_run_pytest_for_group_runs_one_invocation_for_a_small_staged_set(
|
|
2822
|
+
tmp_path: Path,
|
|
2823
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2824
|
+
) -> None:
|
|
2825
|
+
all_recorded_commands = _record_pytest_invocations(monkeypatch, all_exit_codes=[])
|
|
2826
|
+
all_test_paths = _staged_test_paths_under(tmp_path, 3)
|
|
2827
|
+
|
|
2828
|
+
exit_code = gate_module._run_pytest_for_group(tmp_path, all_test_paths, tmp_path)
|
|
2829
|
+
|
|
2830
|
+
assert exit_code == 0
|
|
2831
|
+
assert len(all_recorded_commands) == 1, (
|
|
2832
|
+
"a staged set that fits the budget must behave exactly as before: "
|
|
2833
|
+
"one pytest invocation carrying every path"
|
|
2834
|
+
)
|
|
2835
|
+
|
|
2836
|
+
|
|
2837
|
+
def test_run_pytest_for_group_surfaces_the_first_failing_batch_exit_code(
|
|
2838
|
+
tmp_path: Path,
|
|
2839
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2840
|
+
) -> None:
|
|
2841
|
+
"""A failure in any batch must surface, and every batch must still run so the
|
|
2842
|
+
operator sees the whole staged-test picture rather than the first failure alone.
|
|
2843
|
+
"""
|
|
2844
|
+
all_recorded_commands = _record_pytest_invocations(
|
|
2845
|
+
monkeypatch, all_exit_codes=[0, 1, 2]
|
|
2846
|
+
)
|
|
2847
|
+
all_test_paths = _staged_test_paths_under(tmp_path, 800)
|
|
2848
|
+
|
|
2849
|
+
exit_code = gate_module._run_pytest_for_group(tmp_path, all_test_paths, tmp_path)
|
|
2850
|
+
|
|
2851
|
+
assert exit_code == 1
|
|
2852
|
+
assert len(all_recorded_commands) >= 3
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
def test_run_pytest_for_group_treats_no_tests_collected_as_a_pass_per_batch(
|
|
2856
|
+
tmp_path: Path,
|
|
2857
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2858
|
+
) -> None:
|
|
2859
|
+
_record_pytest_invocations(
|
|
2860
|
+
monkeypatch,
|
|
2861
|
+
all_exit_codes=[gate_module.PYTEST_NO_TESTS_COLLECTED_EXIT_CODE, 0],
|
|
2862
|
+
)
|
|
2863
|
+
all_test_paths = _staged_test_paths_under(tmp_path, 800)
|
|
2864
|
+
|
|
2865
|
+
exit_code = gate_module._run_pytest_for_group(tmp_path, all_test_paths, tmp_path)
|
|
2866
|
+
|
|
2867
|
+
assert exit_code == 0, (
|
|
2868
|
+
"a batch that collects no tests is not a failure; only a real pytest "
|
|
2869
|
+
"failure exit code may block the commit"
|
|
2870
|
+
)
|