claude-dev-env 8.12.0 → 8.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
|
@@ -29,9 +29,10 @@ The frozen process env is only a fallback when that disk read fails (logged once
|
|
|
29
29
|
A probe error does not waive the gate; the live GitHub checks still run.
|
|
30
30
|
|
|
31
31
|
The Codex gate is conditional-required: it demands
|
|
32
|
-
``codex_clean_at == current_head`` only when the
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
``codex_clean_at == current_head`` only when the Codex account picker
|
|
33
|
+
(``scripts/codex_account_choice.py choose``) answers the ``normal`` tier, so
|
|
34
|
+
one signed-in account has room for a review. A ``luna`` or ``wait`` answer, an
|
|
35
|
+
unreadable picker, the codex token, or ``codex_down`` never blocks ready.
|
|
35
36
|
"""
|
|
36
37
|
|
|
37
38
|
from __future__ import annotations
|
|
@@ -62,13 +63,12 @@ from check_convergence_thread_gates import (
|
|
|
62
63
|
_check_no_pending_reviews,
|
|
63
64
|
_count_unresolved_bot_threads,
|
|
64
65
|
)
|
|
65
|
-
from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
66
|
-
USAGE_REPORT_KEY_PERCENT_LEFT,
|
|
67
|
-
)
|
|
68
|
-
from codex_usage_probe import is_codex_review_required, probe_weekly_usage_via_subprocess
|
|
69
66
|
from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
67
|
+
ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS,
|
|
70
68
|
BUGBOT_DOWN_BYPASS_NOTE,
|
|
71
69
|
CLAUDE_JOB_DIR_ENV_VAR_NAME,
|
|
70
|
+
CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS,
|
|
71
|
+
CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND,
|
|
72
72
|
CODEX_BYPASS_DETAIL,
|
|
73
73
|
CODEX_CLEAN_AT_STATE_KEY,
|
|
74
74
|
CODEX_CLEAN_DETAIL_TEMPLATE,
|
|
@@ -76,11 +76,13 @@ from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
|
76
76
|
CODEX_GATE_LABEL,
|
|
77
77
|
CODEX_MISSING_CLEAN_DETAIL_TEMPLATE,
|
|
78
78
|
CODEX_SKIPPED_USAGE_DETAIL,
|
|
79
|
+
CODEX_TIER_KEY,
|
|
80
|
+
CODEX_TIER_NORMAL,
|
|
79
81
|
COPILOT_DOWN_BYPASS_NOTE,
|
|
80
82
|
FIXTURE_DEFAULT_PENDING_DETAIL,
|
|
81
83
|
FIXTURE_DEFAULT_THREADS_DETAIL,
|
|
82
84
|
FIXTURE_KEY_CODEX_CLEAN_AT,
|
|
83
|
-
|
|
85
|
+
FIXTURE_KEY_CODEX_TIER,
|
|
84
86
|
FIXTURE_KEY_HEAD_SHA,
|
|
85
87
|
FIXTURE_KEY_PENDING_REVIEWS_DETAIL,
|
|
86
88
|
FIXTURE_KEY_PENDING_REVIEWS_PASSED,
|
|
@@ -90,6 +92,7 @@ from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
|
90
92
|
FIXTURE_KEY_UNRESOLVED_BOT_THREADS_PASSED,
|
|
91
93
|
MINIMUM_ABBREVIATED_SHA_LENGTH,
|
|
92
94
|
PR_CONVERGE_STATE_FILENAME,
|
|
95
|
+
SHARED_PACKAGE_ROOT_PARENT_INDEX,
|
|
93
96
|
)
|
|
94
97
|
from pr_converge_skill_constants.constants import (
|
|
95
98
|
ALL_COPILOT_CLEAN_REVIEW_STATES,
|
|
@@ -128,7 +131,7 @@ class ConvergenceFixture(NamedTuple):
|
|
|
128
131
|
unresolved_bot_threads_detail: str
|
|
129
132
|
is_no_pending_reviews: bool
|
|
130
133
|
pending_reviews_detail: str
|
|
131
|
-
|
|
134
|
+
codex_tier: str | None
|
|
132
135
|
codex_clean_at: str | None
|
|
133
136
|
|
|
134
137
|
|
|
@@ -333,24 +336,24 @@ def _sha_matches_head(stamp_sha: str, head_sha: str) -> bool:
|
|
|
333
336
|
|
|
334
337
|
def _evaluate_codex_clean(
|
|
335
338
|
*,
|
|
336
|
-
|
|
339
|
+
read_codex_tier: Callable[[], str | None],
|
|
337
340
|
codex_clean_at: str | None,
|
|
338
341
|
head_sha: str,
|
|
339
342
|
) -> tuple[bool, str]:
|
|
340
|
-
"""Return whether the conditional Codex gate passes for the given stamp and
|
|
343
|
+
"""Return whether the conditional Codex gate passes for the given stamp and account tier.
|
|
341
344
|
|
|
342
345
|
::
|
|
343
346
|
|
|
344
|
-
clean stamp on head
|
|
345
|
-
no stamp,
|
|
346
|
-
no stamp,
|
|
347
|
+
clean stamp on head -> pass (picker never runs)
|
|
348
|
+
no stamp, tier luna, wait, or None -> skip (never blocks)
|
|
349
|
+
no stamp, tier normal -> fail
|
|
347
350
|
|
|
348
|
-
A stamp on the current HEAD settles the gate on its own, so the
|
|
349
|
-
stays behind it: the
|
|
350
|
-
already-clean tick has no reason to pay for
|
|
351
|
+
A stamp on the current HEAD settles the gate on its own, so the picker
|
|
352
|
+
stays behind it: the picker starts one Codex app server per account, and
|
|
353
|
+
the common already-clean tick has no reason to pay for them.
|
|
351
354
|
|
|
352
355
|
Args:
|
|
353
|
-
|
|
356
|
+
read_codex_tier: Returns the picker's tier, or None when the picker cannot say.
|
|
354
357
|
codex_clean_at: HEAD SHA where Codex last reported clean, or None.
|
|
355
358
|
head_sha: Current PR HEAD commit SHA.
|
|
356
359
|
|
|
@@ -359,32 +362,38 @@ def _evaluate_codex_clean(
|
|
|
359
362
|
"""
|
|
360
363
|
if codex_clean_at is not None and _sha_matches_head(codex_clean_at, head_sha):
|
|
361
364
|
return True, CODEX_CLEAN_DETAIL_TEMPLATE % _short_sha(head_sha)
|
|
362
|
-
if
|
|
365
|
+
if read_codex_tier() != CODEX_TIER_NORMAL:
|
|
363
366
|
return True, CODEX_SKIPPED_USAGE_DETAIL
|
|
364
367
|
return False, CODEX_MISSING_CLEAN_DETAIL_TEMPLATE % _short_sha(head_sha)
|
|
365
368
|
|
|
366
369
|
|
|
367
|
-
def
|
|
368
|
-
"""
|
|
370
|
+
def _codex_account_picker_path() -> Path:
|
|
371
|
+
"""Return the installed Codex account picker beside this shared tree."""
|
|
372
|
+
shared_root = Path(__file__).resolve().parents[SHARED_PACKAGE_ROOT_PARENT_INDEX]
|
|
373
|
+
return shared_root.joinpath(*ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _read_codex_tier() -> str | None:
|
|
377
|
+
"""Ask the Codex account picker which tier a review runs on, or None when it cannot say."""
|
|
369
378
|
try:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
completed = subprocess.run(
|
|
380
|
+
[
|
|
381
|
+
sys.executable,
|
|
382
|
+
str(_codex_account_picker_path()),
|
|
383
|
+
CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND,
|
|
384
|
+
],
|
|
385
|
+
capture_output=True,
|
|
386
|
+
text=True,
|
|
387
|
+
check=False,
|
|
388
|
+
timeout=CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS,
|
|
389
|
+
)
|
|
390
|
+
answer = json.loads(completed.stdout)
|
|
391
|
+
except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
|
|
381
392
|
return None
|
|
382
|
-
|
|
383
|
-
if isinstance(raw_percent, bool):
|
|
393
|
+
if completed.returncode != 0 or not isinstance(answer, dict):
|
|
384
394
|
return None
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return None
|
|
395
|
+
tier = answer.get(CODEX_TIER_KEY)
|
|
396
|
+
return tier if isinstance(tier, str) else None
|
|
388
397
|
|
|
389
398
|
|
|
390
399
|
def _read_job_state() -> dict[str, object]:
|
|
@@ -468,11 +477,11 @@ def _codex_condition(context: GateContext) -> GateCondition:
|
|
|
468
477
|
if context.is_codex_down:
|
|
469
478
|
return (CODEX_GATE_LABEL, (True, CODEX_BYPASS_DETAIL))
|
|
470
479
|
if context.fixture is not None:
|
|
471
|
-
|
|
480
|
+
fixture_codex_tier = context.fixture.codex_tier
|
|
472
481
|
return (
|
|
473
482
|
CODEX_GATE_LABEL,
|
|
474
483
|
_evaluate_codex_clean(
|
|
475
|
-
|
|
484
|
+
read_codex_tier=lambda: fixture_codex_tier,
|
|
476
485
|
codex_clean_at=context.fixture.codex_clean_at,
|
|
477
486
|
head_sha=context.head_sha,
|
|
478
487
|
),
|
|
@@ -480,7 +489,7 @@ def _codex_condition(context: GateContext) -> GateCondition:
|
|
|
480
489
|
return (
|
|
481
490
|
CODEX_GATE_LABEL,
|
|
482
491
|
_evaluate_codex_clean(
|
|
483
|
-
|
|
492
|
+
read_codex_tier=_read_codex_tier,
|
|
484
493
|
codex_clean_at=context.live_codex_clean_at,
|
|
485
494
|
head_sha=context.head_sha,
|
|
486
495
|
),
|
|
@@ -623,17 +632,13 @@ def _load_convergence_fixture(from_path: Path) -> ConvergenceFixture:
|
|
|
623
632
|
raise ValueError("fixture pending_reviews_passed must be a bool")
|
|
624
633
|
if not isinstance(pending_detail, str):
|
|
625
634
|
raise ValueError("fixture pending_reviews_detail must be a string")
|
|
626
|
-
|
|
635
|
+
codex_tier = payload.get(FIXTURE_KEY_CODEX_TIER)
|
|
627
636
|
codex_clean_at = payload.get(FIXTURE_KEY_CODEX_CLEAN_AT)
|
|
628
|
-
if isinstance(
|
|
629
|
-
raise ValueError("fixture
|
|
630
|
-
if codex_percent_left is not None and not isinstance(codex_percent_left, (int, float)):
|
|
631
|
-
raise ValueError("fixture codex_percent_left must be a number or null")
|
|
637
|
+
if codex_tier is not None and not isinstance(codex_tier, str):
|
|
638
|
+
raise ValueError("fixture codex_tier must be a string or null")
|
|
632
639
|
if codex_clean_at is not None and not isinstance(codex_clean_at, str):
|
|
633
640
|
raise ValueError("fixture codex_clean_at must be a string or null")
|
|
634
|
-
|
|
635
|
-
float(codex_percent_left) if isinstance(codex_percent_left, (int, float)) else None
|
|
636
|
-
)
|
|
641
|
+
typed_codex_tier: str | None = codex_tier if isinstance(codex_tier, str) else None
|
|
637
642
|
typed_codex_clean_at: str | None = codex_clean_at if isinstance(codex_clean_at, str) else None
|
|
638
643
|
return ConvergenceFixture(
|
|
639
644
|
head_sha=head_sha,
|
|
@@ -643,7 +648,7 @@ def _load_convergence_fixture(from_path: Path) -> ConvergenceFixture:
|
|
|
643
648
|
unresolved_bot_threads_detail=threads_detail,
|
|
644
649
|
is_no_pending_reviews=is_pending_ok,
|
|
645
650
|
pending_reviews_detail=pending_detail,
|
|
646
|
-
|
|
651
|
+
codex_tier=typed_codex_tier,
|
|
647
652
|
codex_clean_at=typed_codex_clean_at,
|
|
648
653
|
)
|
|
649
654
|
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
CODEX_BINARY_NAME = "codex"
|
|
6
|
+
WINDOWS_OS_NAME = "nt"
|
|
7
|
+
WINDOWS_COMMAND_SHELL = "cmd"
|
|
8
|
+
WINDOWS_COMMAND_SHELL_RUN_FLAG = "/c"
|
|
9
|
+
ALL_WINDOWS_SCRIPT_SUFFIXES = (".cmd", ".bat")
|
|
6
10
|
EXEC_SUBCOMMAND = "exec"
|
|
7
11
|
REVIEW_SUBCOMMAND = "review"
|
|
8
12
|
HELP_FLAG = "--help"
|
|
@@ -45,7 +45,7 @@ FIXTURE_KEY_UNRESOLVED_BOT_THREADS_PASSED: str = "unresolved_bot_threads_passed"
|
|
|
45
45
|
FIXTURE_KEY_UNRESOLVED_BOT_THREADS_DETAIL: str = "unresolved_bot_threads_detail"
|
|
46
46
|
FIXTURE_KEY_PENDING_REVIEWS_PASSED: str = "pending_reviews_passed"
|
|
47
47
|
FIXTURE_KEY_PENDING_REVIEWS_DETAIL: str = "pending_reviews_detail"
|
|
48
|
-
|
|
48
|
+
FIXTURE_KEY_CODEX_TIER: str = "codex_tier"
|
|
49
49
|
FIXTURE_KEY_CODEX_CLEAN_AT: str = "codex_clean_at"
|
|
50
50
|
FIXTURE_DEFAULT_THREADS_DETAIL: str = "0 unresolved"
|
|
51
51
|
FIXTURE_DEFAULT_PENDING_DETAIL: str = "none pending"
|
|
@@ -68,3 +68,12 @@ PR_CONVERGE_STATE_FILENAME: str = "pr-converge-state.json"
|
|
|
68
68
|
CODEX_CLEAN_AT_STATE_KEY: str = "codex_clean_at"
|
|
69
69
|
CODEX_DOWN_STATE_KEY: str = "codex_down"
|
|
70
70
|
MINIMUM_ABBREVIATED_SHA_LENGTH: int = 7
|
|
71
|
+
SHARED_PACKAGE_ROOT_PARENT_INDEX: int = 3
|
|
72
|
+
ALL_CODEX_ACCOUNT_PICKER_RELATIVE_PARTS: tuple[str, ...] = (
|
|
73
|
+
"scripts",
|
|
74
|
+
"codex_account_choice.py",
|
|
75
|
+
)
|
|
76
|
+
CODEX_ACCOUNT_PICKER_CHOOSE_COMMAND: str = "choose"
|
|
77
|
+
CODEX_ACCOUNT_PICK_TIMEOUT_SECONDS: float = 150.0
|
|
78
|
+
CODEX_TIER_KEY: str = "tier"
|
|
79
|
+
CODEX_TIER_NORMAL: str = "normal"
|
|
@@ -44,14 +44,9 @@ from process_tree_kill import ( # noqa: E402
|
|
|
44
44
|
terminate_process_tree,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
48
|
-
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
49
|
-
WINDOWS_COMMAND_SHELL,
|
|
50
|
-
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
51
|
-
WINDOWS_OS_NAME,
|
|
52
|
-
)
|
|
53
47
|
from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
54
48
|
ALL_SHAPE_PROBE_REQUIRED_FLAGS,
|
|
49
|
+
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
55
50
|
BASE_TARGET_FLAG,
|
|
56
51
|
CAPTURE_STREAMS_KEYWORD,
|
|
57
52
|
CHECK_KEYWORD,
|
|
@@ -88,6 +83,9 @@ from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
|
88
83
|
UTF8_ENCODING,
|
|
89
84
|
VERSION_FLAG,
|
|
90
85
|
VERSION_PROBE_PATTERN,
|
|
86
|
+
WINDOWS_COMMAND_SHELL,
|
|
87
|
+
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
88
|
+
WINDOWS_OS_NAME,
|
|
91
89
|
)
|
|
92
90
|
|
|
93
91
|
from subprocess_window_access import hidden_window_creation_flags
|
|
@@ -107,14 +107,14 @@ def _raise_if_called(gate_label: str) -> Callable[..., tuple[bool, str]]:
|
|
|
107
107
|
return _stub
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
def
|
|
110
|
+
def _unread_codex_tier() -> str | None:
|
|
111
111
|
return None
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def _patch_all_gates_passing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
115
115
|
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _passing_head_sha)
|
|
116
116
|
monkeypatch.setattr(
|
|
117
|
-
check_convergence, "
|
|
117
|
+
check_convergence, "_read_codex_tier", _unread_codex_tier
|
|
118
118
|
)
|
|
119
119
|
for each_gate_name in _ALL_LEAF_GATE_NAMES:
|
|
120
120
|
monkeypatch.setattr(check_convergence, each_gate_name, _passing_gate)
|
|
@@ -135,7 +135,7 @@ def should_pass_when_clean_bugteam_review_present_on_current_head(
|
|
|
135
135
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
136
136
|
)
|
|
137
137
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
138
|
-
owner="
|
|
138
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
139
139
|
)
|
|
140
140
|
assert passed is True
|
|
141
141
|
assert "clean bugteam audit" in detail
|
|
@@ -157,7 +157,7 @@ def should_fail_when_dirty_bugteam_review_present_on_current_head(
|
|
|
157
157
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
158
158
|
)
|
|
159
159
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
160
|
-
owner="
|
|
160
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
161
161
|
)
|
|
162
162
|
assert passed is False
|
|
163
163
|
assert "dirty bugteam audit" in detail
|
|
@@ -185,7 +185,7 @@ def should_fail_when_no_bugteam_review_present_on_current_head(
|
|
|
185
185
|
check_convergence, "_gh_api_paginated", _make_stub_gh_paginated(reviews_payload)
|
|
186
186
|
)
|
|
187
187
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
188
|
-
owner="
|
|
188
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
189
189
|
)
|
|
190
190
|
assert passed is False
|
|
191
191
|
assert "no bugteam review found" in detail
|
|
@@ -204,7 +204,7 @@ def should_fail_with_shape_detail_when_gh_returns_non_list_payload(
|
|
|
204
204
|
check_convergence, "_gh_api_paginated", stub_gh_api_paginated_returning_object
|
|
205
205
|
)
|
|
206
206
|
passed, detail = check_convergence._check_bugteam_clean(
|
|
207
|
-
owner="
|
|
207
|
+
owner="octocat", repo="tests", number=42, head_sha=CURRENT_HEAD_SHA
|
|
208
208
|
)
|
|
209
209
|
assert passed is False
|
|
210
210
|
assert "unexpected gh api response shape" in detail
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Fixture-driven tests for the conditional Codex convergence gate.
|
|
2
2
|
|
|
3
|
-
Covers: required-and-clean, required-and-dirty, skipped-by-
|
|
4
|
-
skipped-by-token, skipped-by-down
|
|
5
|
-
|
|
3
|
+
Covers: required-and-clean, required-and-dirty, skipped-by-tier,
|
|
4
|
+
skipped-by-token, skipped-by-down, and the read of the Codex account picker's
|
|
5
|
+
``choose`` answer. The required tier comes from ``CODEX_TIER_NORMAL``.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -16,10 +16,8 @@ from types import ModuleType
|
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
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
19
|
from pr_converge_scripts_constants.convergence_gate_constants import (
|
|
20
|
+
CODEX_TIER_NORMAL,
|
|
23
21
|
MINIMUM_ABBREVIATED_SHA_LENGTH,
|
|
24
22
|
)
|
|
25
23
|
|
|
@@ -33,8 +31,7 @@ CLEAN_BODY = (
|
|
|
33
31
|
)
|
|
34
32
|
HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
35
33
|
OTHER_SHA = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
36
|
-
|
|
37
|
-
PERCENT_AT_THRESHOLD = float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT)
|
|
34
|
+
ALL_TIERS_WITHOUT_ROOM_FOR_REVIEW = ("luna", "wait")
|
|
38
35
|
|
|
39
36
|
|
|
40
37
|
def _load_module() -> ModuleType:
|
|
@@ -64,7 +61,7 @@ check_convergence = _load_module()
|
|
|
64
61
|
def _write_fixture(
|
|
65
62
|
tmp_path: Path,
|
|
66
63
|
*,
|
|
67
|
-
|
|
64
|
+
codex_tier: str | None,
|
|
68
65
|
codex_clean_at: str | None,
|
|
69
66
|
filename: str = "codex-gate.json",
|
|
70
67
|
) -> Path:
|
|
@@ -83,7 +80,7 @@ def _write_fixture(
|
|
|
83
80
|
"unresolved_bot_threads_detail": "0 unresolved",
|
|
84
81
|
"pending_reviews_passed": True,
|
|
85
82
|
"pending_reviews_detail": "none pending",
|
|
86
|
-
"
|
|
83
|
+
"codex_tier": codex_tier,
|
|
87
84
|
"codex_clean_at": codex_clean_at,
|
|
88
85
|
}
|
|
89
86
|
fixture_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
@@ -95,7 +92,7 @@ def should_pass_when_codex_required_and_clean(
|
|
|
95
92
|
) -> None:
|
|
96
93
|
fixture_path = _write_fixture(
|
|
97
94
|
tmp_path,
|
|
98
|
-
|
|
95
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
99
96
|
codex_clean_at=HEAD_SHA,
|
|
100
97
|
filename="required-clean.json",
|
|
101
98
|
)
|
|
@@ -120,20 +117,18 @@ def should_pass_when_codex_required_and_clean(
|
|
|
120
117
|
assert "All pre-conditions met" in captured
|
|
121
118
|
|
|
122
119
|
|
|
123
|
-
def
|
|
120
|
+
def should_pass_without_running_the_picker_when_the_clean_stamp_is_on_head(
|
|
124
121
|
monkeypatch: pytest.MonkeyPatch,
|
|
125
122
|
) -> None:
|
|
126
|
-
def
|
|
123
|
+
def _picker_that_must_not_run() -> str | None:
|
|
127
124
|
raise AssertionError(
|
|
128
|
-
"the
|
|
125
|
+
"the Codex account picker must not run when codex_clean_at is on HEAD"
|
|
129
126
|
)
|
|
130
127
|
|
|
131
|
-
monkeypatch.setattr(
|
|
132
|
-
check_convergence, "_probe_codex_percent_left", _probe_that_must_not_run
|
|
133
|
-
)
|
|
128
|
+
monkeypatch.setattr(check_convergence, "_read_codex_tier", _picker_that_must_not_run)
|
|
134
129
|
|
|
135
130
|
is_passed, detail = check_convergence._evaluate_codex_clean(
|
|
136
|
-
|
|
131
|
+
read_codex_tier=check_convergence._read_codex_tier,
|
|
137
132
|
codex_clean_at=HEAD_SHA,
|
|
138
133
|
head_sha=HEAD_SHA,
|
|
139
134
|
)
|
|
@@ -151,7 +146,7 @@ def should_fail_when_codex_clean_stamp_is_shorter_than_an_abbreviated_sha(
|
|
|
151
146
|
) -> None:
|
|
152
147
|
fixture_path = _write_fixture(
|
|
153
148
|
tmp_path,
|
|
154
|
-
|
|
149
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
155
150
|
codex_clean_at=HEAD_SHA[:1],
|
|
156
151
|
filename="required-short-stamp.json",
|
|
157
152
|
)
|
|
@@ -179,7 +174,7 @@ def should_pass_when_codex_clean_stamp_is_an_abbreviated_head_sha(
|
|
|
179
174
|
) -> None:
|
|
180
175
|
fixture_path = _write_fixture(
|
|
181
176
|
tmp_path,
|
|
182
|
-
|
|
177
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
183
178
|
codex_clean_at=HEAD_SHA[:MINIMUM_ABBREVIATED_SHA_LENGTH],
|
|
184
179
|
filename="required-abbreviated-stamp.json",
|
|
185
180
|
)
|
|
@@ -207,7 +202,7 @@ def should_fail_when_codex_required_and_dirty(
|
|
|
207
202
|
) -> None:
|
|
208
203
|
fixture_path = _write_fixture(
|
|
209
204
|
tmp_path,
|
|
210
|
-
|
|
205
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
211
206
|
codex_clean_at=OTHER_SHA,
|
|
212
207
|
filename="required-dirty.json",
|
|
213
208
|
)
|
|
@@ -232,14 +227,15 @@ def should_fail_when_codex_required_and_dirty(
|
|
|
232
227
|
assert "do not mark ready" in captured
|
|
233
228
|
|
|
234
229
|
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
@pytest.mark.parametrize("tier_without_room", ALL_TIERS_WITHOUT_ROOM_FOR_REVIEW)
|
|
231
|
+
def should_skip_when_the_picker_names_no_account_with_room_for_review(
|
|
232
|
+
tier_without_room: str, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
237
233
|
) -> None:
|
|
238
234
|
fixture_path = _write_fixture(
|
|
239
235
|
tmp_path,
|
|
240
|
-
|
|
236
|
+
codex_tier=tier_without_room,
|
|
241
237
|
codex_clean_at=None,
|
|
242
|
-
filename="skipped-
|
|
238
|
+
filename="skipped-tier.json",
|
|
243
239
|
)
|
|
244
240
|
exit_code = check_convergence.main(
|
|
245
241
|
[
|
|
@@ -261,12 +257,12 @@ def should_skip_when_usage_at_or_below_threshold(
|
|
|
261
257
|
assert "skipped (codex review not required)" in captured
|
|
262
258
|
|
|
263
259
|
|
|
264
|
-
def
|
|
260
|
+
def should_skip_when_codex_tier_is_null(
|
|
265
261
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
266
262
|
) -> None:
|
|
267
263
|
fixture_path = _write_fixture(
|
|
268
264
|
tmp_path,
|
|
269
|
-
|
|
265
|
+
codex_tier=None,
|
|
270
266
|
codex_clean_at=None,
|
|
271
267
|
filename="skipped-null.json",
|
|
272
268
|
)
|
|
@@ -297,7 +293,7 @@ def should_bypass_when_codex_token_disables_reviewer(
|
|
|
297
293
|
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
|
|
298
294
|
fixture_path = _write_fixture(
|
|
299
295
|
tmp_path,
|
|
300
|
-
|
|
296
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
301
297
|
codex_clean_at=None,
|
|
302
298
|
filename="skipped-token.json",
|
|
303
299
|
)
|
|
@@ -326,7 +322,7 @@ def should_bypass_when_codex_down_flag_set(
|
|
|
326
322
|
) -> None:
|
|
327
323
|
fixture_path = _write_fixture(
|
|
328
324
|
tmp_path,
|
|
329
|
-
|
|
325
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
330
326
|
codex_clean_at=None,
|
|
331
327
|
filename="skipped-down.json",
|
|
332
328
|
)
|
|
@@ -433,7 +429,7 @@ def should_bypass_when_job_state_codex_down_is_true(
|
|
|
433
429
|
)
|
|
434
430
|
fixture_path = _write_fixture(
|
|
435
431
|
tmp_path,
|
|
436
|
-
|
|
432
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
437
433
|
codex_clean_at=None,
|
|
438
434
|
filename="job-state-codex-down.json",
|
|
439
435
|
)
|
|
@@ -470,7 +466,7 @@ def should_fail_when_job_state_lacks_codex_down_and_clean_stamp(
|
|
|
470
466
|
)
|
|
471
467
|
fixture_path = _write_fixture(
|
|
472
468
|
tmp_path,
|
|
473
|
-
|
|
469
|
+
codex_tier=CODEX_TIER_NORMAL,
|
|
474
470
|
codex_clean_at=None,
|
|
475
471
|
filename="job-state-no-down.json",
|
|
476
472
|
)
|
|
@@ -515,3 +511,49 @@ def should_accept_codex_clean_at_flag_in_parsed_arguments() -> None:
|
|
|
515
511
|
]
|
|
516
512
|
)
|
|
517
513
|
assert arguments.codex_clean_at == HEAD_SHA
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _install_fake_picker(
|
|
517
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, picker_source: str
|
|
518
|
+
) -> None:
|
|
519
|
+
picker_path = tmp_path / "codex_account_choice.py"
|
|
520
|
+
picker_path.write_text(picker_source, encoding="utf-8")
|
|
521
|
+
monkeypatch.setattr(check_convergence, "_codex_account_picker_path", lambda: picker_path)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def should_read_the_tier_the_picker_chooses(
|
|
525
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
526
|
+
) -> None:
|
|
527
|
+
_install_fake_picker(
|
|
528
|
+
tmp_path,
|
|
529
|
+
monkeypatch,
|
|
530
|
+
"import json, sys\n"
|
|
531
|
+
"assert sys.argv[1:] == ['choose']\n"
|
|
532
|
+
"print(json.dumps({'tier': 'normal', 'account': 'codex-2'}))\n",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
assert check_convergence._read_codex_tier() == CODEX_TIER_NORMAL
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@pytest.mark.parametrize(
|
|
539
|
+
"picker_source",
|
|
540
|
+
[
|
|
541
|
+
"import sys\nprint('{\"tier\": \"normal\"}')\nsys.exit(1)\n",
|
|
542
|
+
"print('not json')\n",
|
|
543
|
+
"print('[\"normal\"]')\n",
|
|
544
|
+
"print('{\"tier\": 1}')\n",
|
|
545
|
+
],
|
|
546
|
+
)
|
|
547
|
+
def should_read_no_tier_when_the_picker_fails_or_answers_out_of_shape(
|
|
548
|
+
picker_source: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
549
|
+
) -> None:
|
|
550
|
+
_install_fake_picker(tmp_path, monkeypatch, picker_source)
|
|
551
|
+
|
|
552
|
+
assert check_convergence._read_codex_tier() is None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def should_locate_the_packaged_codex_account_picker() -> None:
|
|
556
|
+
picker_path = check_convergence._codex_account_picker_path()
|
|
557
|
+
|
|
558
|
+
assert picker_path.name == "codex_account_choice.py"
|
|
559
|
+
assert picker_path.is_file()
|
|
@@ -118,14 +118,14 @@ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
|
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def
|
|
121
|
+
def _unread_codex_tier() -> str | None:
|
|
122
122
|
return None
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
126
126
|
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
|
|
127
127
|
monkeypatch.setattr(
|
|
128
|
-
check_convergence, "
|
|
128
|
+
check_convergence, "_read_codex_tier", _unread_codex_tier
|
|
129
129
|
)
|
|
130
130
|
for each_gate_name in _ALL_LEAF_GATE_NAMES:
|
|
131
131
|
monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
|
|
@@ -127,10 +127,10 @@ def test_path_contains_glob_metacharacters_accepts_windows_paths_with_parens() -
|
|
|
127
127
|
`(`, `)`, and `,` are not glob metacharacters in Claude Code's permission
|
|
128
128
|
rule matching. Including them in the metacharacter set causes
|
|
129
129
|
get_current_project_path to raise ValueError for any user whose home
|
|
130
|
-
directory contains parentheses (e.g. `C:/Users/
|
|
130
|
+
directory contains parentheses (e.g. `C:/Users/Example (Admin)/...`).
|
|
131
131
|
"""
|
|
132
132
|
assert common.path_contains_glob_metacharacters("C:/Program Files (x86)/app") is False
|
|
133
|
-
assert common.path_contains_glob_metacharacters("C:/Users/
|
|
133
|
+
assert common.path_contains_glob_metacharacters("C:/Users/Example (Admin)/project") is False
|
|
134
134
|
assert common.path_contains_glob_metacharacters("C:/Projects/a,b/file.py") is False
|
|
135
135
|
|
|
136
136
|
|
|
@@ -227,7 +227,7 @@ test('generated advisory shims keep temporary Git commit and push available afte
|
|
|
227
227
|
const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
|
|
228
228
|
const repositoryRoot = join(temporaryRoot, 'repository');
|
|
229
229
|
const bareRepositoryRoot = join(temporaryRoot, 'remote.git');
|
|
230
|
-
const targetRemote = 'https://github.com/
|
|
230
|
+
const targetRemote = 'https://github.com/example-owner/example-repo.git';
|
|
231
231
|
try {
|
|
232
232
|
writeFileSync(
|
|
233
233
|
join(gitHooksDirectory, 'broken_advisory.py'),
|
|
@@ -159,15 +159,10 @@ export const CODEX_RULES_SHIPPED_FILE_NAME = 'claude-dev-env.rules';
|
|
|
159
159
|
export const DEFAULT_CURSOR_DIRECTORY_NAME = '.cursor';
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* Directory name under the Cursor home
|
|
162
|
+
* Directory name for rule files, under the Cursor home and under the agents home.
|
|
163
163
|
*/
|
|
164
164
|
export const CURSOR_RULES_DIRECTORY_NAME = 'rules';
|
|
165
165
|
|
|
166
|
-
/**
|
|
167
|
-
* Installed script that writes Cursor `.mdc` files from Claude rules.
|
|
168
|
-
*/
|
|
169
|
-
export const CURSOR_SYNC_SCRIPT_FILE_NAME = 'sync_to_cursor.py';
|
|
170
|
-
|
|
171
166
|
/**
|
|
172
167
|
* Windows Python launcher command the installer may bake into hook settings.
|
|
173
168
|
*/
|