claude-dev-env 8.12.0 → 8.14.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/team-advisor/SKILL.md +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/advisor-protocol.md +19 -19
- package/_shared/advisor/reference/advisor-block.md +1 -1
- package/_shared/advisor/reference/astra-rung.md +11 -11
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +2 -2
- package/_shared/advisor/reference/spawn-walk-log.md +4 -4
- package/_shared/advisor/reference/third-party-bind.md +7 -7
- package/_shared/advisor/reference/warm-up.md +3 -3
- 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/advisor_route_constants.py +2 -2
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -3
- package/_shared/advisor/scripts/model_tier_run_validator.py +8 -9
- 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/advisor/scripts/tests/test_model_tier_run_validator.py +91 -91
- 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
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Ask the Codex account picker for an account before an Astra advisor request."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import importlib
|
|
6
5
|
import json
|
|
7
6
|
import math
|
|
8
7
|
import subprocess
|
|
@@ -12,11 +11,13 @@ from dataclasses import dataclass
|
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
|
|
14
13
|
from advisor_scripts_constants.astra_advisor_constants import (
|
|
14
|
+
ACCOUNT_PICKER_CHOOSE_COMMAND,
|
|
15
|
+
ASTRA_ACCOUNT_PICK_TIMEOUT_REASON,
|
|
16
|
+
ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS,
|
|
15
17
|
ASTRA_FALLBACK_KIND_BROKEN,
|
|
16
18
|
ASTRA_FALLBACK_KIND_DECLINED,
|
|
17
19
|
ASTRA_PREFLIGHT_FAILURE_REASON,
|
|
18
|
-
|
|
19
|
-
ASTRA_USAGE_PROBE_TIMEOUT_SECONDS,
|
|
20
|
+
CODEX_TIER_NORMAL,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
@@ -26,6 +27,7 @@ class AstraPreflight:
|
|
|
26
27
|
percent_left: float | None
|
|
27
28
|
reason: str
|
|
28
29
|
fallback_kind: str | None = None
|
|
30
|
+
codex_home: Path | None = None
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def _preflight_fallback(
|
|
@@ -34,82 +36,82 @@ def _preflight_fallback(
|
|
|
34
36
|
return AstraPreflight(False, percent_left, reason, fallback_kind)
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return importlib.import_module("codex_usage_probe").is_codex_review_required
|
|
39
|
+
def _broken(detail: str) -> AstraPreflight:
|
|
40
|
+
return _preflight_fallback(
|
|
41
|
+
f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {detail}", None, ASTRA_FALLBACK_KIND_BROKEN
|
|
42
|
+
)
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
def
|
|
45
|
-
try:
|
|
46
|
-
report = json.loads(stdout_text)
|
|
47
|
-
except (TypeError, json.JSONDecodeError):
|
|
48
|
-
return None, "usage report is malformed"
|
|
49
|
-
if not isinstance(report, dict):
|
|
50
|
-
return None, "usage report is malformed"
|
|
51
|
-
raw_percent = report.get("percent_left")
|
|
45
|
+
def _finite_percent(raw_percent: object) -> float | None:
|
|
52
46
|
if isinstance(raw_percent, bool) or not isinstance(raw_percent, (int, float)):
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
return None
|
|
48
|
+
return float(raw_percent) if math.isfinite(raw_percent) else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _preflight_from_answer(field_by_name: dict[str, object]) -> AstraPreflight:
|
|
52
|
+
if not isinstance(field_by_name.get("tier"), str):
|
|
53
|
+
return _broken("picker answer is malformed")
|
|
54
|
+
if field_by_name["tier"] != CODEX_TIER_NORMAL:
|
|
55
|
+
reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: no Codex account has room ({field_by_name.get('reason')})"
|
|
56
|
+
return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_DECLINED)
|
|
57
|
+
codex_home = field_by_name.get("codex_home")
|
|
58
|
+
percent_left = _finite_percent(field_by_name.get("percent_left"))
|
|
59
|
+
if not isinstance(codex_home, str) or not codex_home or percent_left is None:
|
|
60
|
+
return _broken("picker answer names no Codex home with room")
|
|
61
|
+
reason = f"{field_by_name.get('account')} has {percent_left:.0f}% left"
|
|
62
|
+
return AstraPreflight(True, percent_left, reason, codex_home=Path(codex_home))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run_picker(
|
|
66
|
+
picker_path: Path,
|
|
62
67
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
63
68
|
) -> subprocess.CompletedProcess[str]:
|
|
64
69
|
return process_runner(
|
|
65
|
-
[sys.executable, str(
|
|
70
|
+
[sys.executable, str(picker_path), ACCOUNT_PICKER_CHOOSE_COMMAND],
|
|
66
71
|
capture_output=True,
|
|
67
72
|
text=True,
|
|
68
73
|
check=False,
|
|
69
74
|
shell=False,
|
|
70
|
-
timeout=
|
|
75
|
+
timeout=ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS,
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
|
|
74
|
-
def
|
|
75
|
-
probe_path: Path, completed: subprocess.CompletedProcess[str]
|
|
76
|
-
) -> AstraPreflight:
|
|
79
|
+
def _preflight_from_picker(completed: subprocess.CompletedProcess[str]) -> AstraPreflight:
|
|
77
80
|
if completed.returncode != 0:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return _preflight_fallback(reason, percent_left, ASTRA_FALLBACK_KIND_DECLINED)
|
|
87
|
-
return AstraPreflight(True, percent_left, "usage meter is above the Astra gate")
|
|
81
|
+
return _broken(f"picker exit {completed.returncode}")
|
|
82
|
+
try:
|
|
83
|
+
answer = json.loads(completed.stdout)
|
|
84
|
+
except (TypeError, json.JSONDecodeError):
|
|
85
|
+
return _broken("picker answer is malformed")
|
|
86
|
+
if not isinstance(answer, dict):
|
|
87
|
+
return _broken("picker answer is malformed")
|
|
88
|
+
return _preflight_from_answer(answer)
|
|
88
89
|
|
|
89
90
|
|
|
90
91
|
def run_astra_preflight(
|
|
91
|
-
|
|
92
|
+
picker_path: Path,
|
|
92
93
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
93
94
|
) -> AstraPreflight:
|
|
94
|
-
"""Run the
|
|
95
|
+
"""Run the Codex account picker and decide whether Astra may bind.
|
|
96
|
+
|
|
97
|
+
::
|
|
98
|
+
|
|
99
|
+
tier normal, codex-2, 62% left -> eligible, codex_home of codex-2
|
|
100
|
+
tier luna or wait -> declined
|
|
101
|
+
picker exit 1, bad JSON, timeout -> broken
|
|
95
102
|
|
|
96
103
|
Args:
|
|
97
|
-
|
|
98
|
-
process_runner: Callable that runs the
|
|
104
|
+
picker_path: Path to ``codex_account_choice.py``.
|
|
105
|
+
process_runner: Callable that runs the picker.
|
|
99
106
|
|
|
100
107
|
Returns:
|
|
101
|
-
Eligibility,
|
|
108
|
+
Eligibility, the chosen account's percent left and Codex home, reason, and fallback kind.
|
|
102
109
|
"""
|
|
103
110
|
try:
|
|
104
|
-
|
|
105
|
-
return _preflight_from_probe(probe_path, completed)
|
|
111
|
+
return _preflight_from_picker(_run_picker(picker_path, process_runner))
|
|
106
112
|
except subprocess.TimeoutExpired as error:
|
|
107
113
|
return _preflight_fallback(
|
|
108
|
-
f"{
|
|
109
|
-
)
|
|
110
|
-
except (OSError, subprocess.SubprocessError, ImportError, AttributeError, TypeError, ValueError) as error:
|
|
111
|
-
return _preflight_fallback(
|
|
112
|
-
f"{ASTRA_PREFLIGHT_FAILURE_REASON}: {error}",
|
|
113
|
-
None,
|
|
114
|
-
ASTRA_FALLBACK_KIND_BROKEN,
|
|
114
|
+
f"{ASTRA_ACCOUNT_PICK_TIMEOUT_REASON}: {error}", None, ASTRA_FALLBACK_KIND_BROKEN
|
|
115
115
|
)
|
|
116
|
+
except (OSError, subprocess.SubprocessError, TypeError, ValueError) as error:
|
|
117
|
+
return _broken(str(error))
|
|
@@ -6,8 +6,8 @@ import json
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
ADVISOR_MODEL_TIER: str = "Astra"
|
|
9
|
-
ADVISOR_FALLBACK_TIER: str = "
|
|
10
|
-
ADVISOR_FALLBACK_RESULT: str = "
|
|
9
|
+
ADVISOR_FALLBACK_TIER: str = "Opus"
|
|
10
|
+
ADVISOR_FALLBACK_RESULT: str = "opus"
|
|
11
11
|
ADVISOR_EFFORT_ENV_VAR: str = "ADVISOR_EFFORT"
|
|
12
12
|
ADVISOR_ROUTING_TIMEOUT_SECONDS: int = 10
|
|
13
13
|
|
|
@@ -13,20 +13,20 @@ CODEX_PROMPT_FROM_STDIN: str = "-"
|
|
|
13
13
|
CODEX_EXEC_SUBCOMMAND: str = "exec"
|
|
14
14
|
CODEX_RESUME_SUBCOMMAND: str = "resume"
|
|
15
15
|
CODEX_SANDBOX_FLAG: str = "--sandbox"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
CODEX_HOME_ENV_VAR: str = "CODEX_HOME"
|
|
17
|
+
ALL_ACCOUNT_PICKER_RELATIVE_PARTS: tuple[str, ...] = ("scripts", "codex_account_choice.py")
|
|
18
|
+
SHARED_PACKAGE_ROOT_PARENT_INDEX: int = 2
|
|
19
|
+
ACCOUNT_PICKER_CHOOSE_COMMAND: str = "choose"
|
|
20
|
+
CODEX_TIER_NORMAL: str = "normal"
|
|
21
21
|
ASTRA_SESSION_ID_METAVAR: str = "SESSION_ID"
|
|
22
22
|
ASTRA_CODEX_TIMEOUT_SECONDS: float = 120.0
|
|
23
|
-
|
|
23
|
+
ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS: float = 150.0
|
|
24
24
|
ASTRA_ENV_VAR: str = "ADVISOR_ASTRA"
|
|
25
25
|
ALL_ASTRA_TRUTHY_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
26
|
-
ASTRA_PREFLIGHT_FAILURE_REASON: str = "astra preflight
|
|
26
|
+
ASTRA_PREFLIGHT_FAILURE_REASON: str = "astra preflight found no Codex account to bind"
|
|
27
27
|
ASTRA_BIND_FAILURE_REASON: str = "Codex Astra bind failed"
|
|
28
28
|
ASTRA_REPLY_FAILURE_REASON: str = "Codex Astra returned no advisor guidance"
|
|
29
|
-
|
|
29
|
+
ASTRA_ACCOUNT_PICK_TIMEOUT_REASON: str = "Codex Astra account pick timed out"
|
|
30
30
|
ASTRA_CODEX_TIMEOUT_REASON: str = "Codex Astra request timed out"
|
|
31
31
|
ASTRA_MALFORMED_JSONL_REASON: str = "Codex Astra returned malformed JSONL"
|
|
32
32
|
ASTRA_MISSING_SESSION_REASON: str = "Codex Astra returned no session id"
|
|
@@ -43,8 +43,8 @@ SESSION_IDENTITY_WORD_PATTERN: str = r"[a-z0-9]+"
|
|
|
43
43
|
HOST_PROFILE_JSON_KEY: str = "host_profile"
|
|
44
44
|
|
|
45
45
|
ALL_MODEL_TIERS: tuple[str, ...] = (
|
|
46
|
+
"Fable",
|
|
46
47
|
ADVISOR_FALLBACK_TIER,
|
|
47
|
-
"Opus",
|
|
48
48
|
"Sonnet",
|
|
49
49
|
"Haiku",
|
|
50
50
|
)
|
|
@@ -58,8 +58,8 @@ ALL_KNOWN_TIER_NAMES: tuple[str, ...] = (
|
|
|
58
58
|
ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS: int = 120
|
|
59
59
|
|
|
60
60
|
ALL_CLI_MODEL_ID_BY_TIER: dict[str, str] = {
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
"Fable": "fable",
|
|
62
|
+
ADVISOR_FALLBACK_TIER: "opus",
|
|
63
63
|
"Sonnet": "sonnet",
|
|
64
64
|
"Haiku": "haiku",
|
|
65
65
|
THIRD_PARTY_MODEL_TIER: "third-party",
|
|
@@ -7,17 +7,17 @@ This validator reads that log back and checks its invariants.
|
|
|
7
7
|
|
|
8
8
|
ladder_walk = ModelTierRun(
|
|
9
9
|
own_tier="Opus",
|
|
10
|
-
candidate_tiers=["
|
|
11
|
-
attempts=[{"tier": "
|
|
12
|
-
selected_tier="
|
|
10
|
+
candidate_tiers=["Opus"],
|
|
11
|
+
attempts=[{"tier": "Opus", "result": "spawned"}],
|
|
12
|
+
selected_tier="Opus",
|
|
13
13
|
)
|
|
14
14
|
validate_model_tier_run(ladder_walk) # ok: returns None, raises nothing
|
|
15
15
|
|
|
16
16
|
cli_bind = ModelTierRun(
|
|
17
17
|
own_tier="Opus",
|
|
18
|
-
candidate_tiers=["
|
|
19
|
-
attempts=[{"tier": "
|
|
20
|
-
selected_tier="
|
|
18
|
+
candidate_tiers=["Opus"],
|
|
19
|
+
attempts=[{"tier": "Opus", "result": "cli"}],
|
|
20
|
+
selected_tier="Opus",
|
|
21
21
|
)
|
|
22
22
|
validate_model_tier_run(cli_bind) # ok: third-party-host CLI Claude-chain bind
|
|
23
23
|
|
|
@@ -330,10 +330,9 @@ def validate_model_tier_run(run: ModelTierRun) -> None:
|
|
|
330
330
|
validate_model_tier_run(cli_bind) # ok: CLI Claude-chain bind
|
|
331
331
|
validate_model_tier_run(broken_log) # flag: ModelTierRunError
|
|
332
332
|
|
|
333
|
-
Candidate tiers are
|
|
333
|
+
Candidate tiers are Opus, plus Astra when ``is_astra_enabled`` is true, on
|
|
334
334
|
Claude and ThirdParty hosts. A Codex host walks Astra only. Consumer
|
|
335
|
-
``own_tier`` is recorded and must be a known tier.
|
|
336
|
-
to the advisor walk. Tries walk that list in order. Early stop only
|
|
335
|
+
``own_tier`` is recorded and must be a known tier. Tries walk that list in order. Early stop only
|
|
337
336
|
after ``spawned``, ``cli``, or Astra ``codex`` (and Astra ``spawned`` on a
|
|
338
337
|
Codex host). A null selected_tier requires a full walk plus
|
|
339
338
|
fallback_reason.
|
|
@@ -31,9 +31,15 @@ def _load_astra_module() -> ModuleType:
|
|
|
31
31
|
astra_advisor = _load_astra_module()
|
|
32
32
|
|
|
33
33
|
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
PICKER_PATH = SCRIPTS_ROOT.parents[2] / "scripts" / "codex_account_choice.py"
|
|
35
|
+
PICKED_CODEX_HOME = Path("codex-profiles") / "codex-2"
|
|
36
|
+
NORMAL_ANSWER = {
|
|
37
|
+
"tier": "normal",
|
|
38
|
+
"account": "codex-2",
|
|
39
|
+
"codex_home": str(PICKED_CODEX_HOME),
|
|
40
|
+
"percent_left": 90,
|
|
41
|
+
"reason": "codex-2 has 90% left",
|
|
42
|
+
}
|
|
37
43
|
ENABLED_SETTINGS = {astra_advisor.ASTRA_ENV_VAR: "1"}
|
|
38
44
|
|
|
39
45
|
|
|
@@ -42,9 +48,9 @@ def _codex_on_search_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
42
48
|
monkeypatch.setattr(astra_advisor.shutil, "which", lambda name: "codex")
|
|
43
49
|
|
|
44
50
|
|
|
45
|
-
def
|
|
51
|
+
def _pick(payload: object, returncode: int = 0) -> subprocess.CompletedProcess[str]:
|
|
46
52
|
return subprocess.CompletedProcess(
|
|
47
|
-
[sys.executable, str(
|
|
53
|
+
[sys.executable, str(PICKER_PATH), "choose"],
|
|
48
54
|
returncode,
|
|
49
55
|
json.dumps(payload),
|
|
50
56
|
"",
|
|
@@ -66,14 +72,18 @@ def _events(session_id: str = "thread-1", guidance: str = "PLAN\ninspect") -> st
|
|
|
66
72
|
|
|
67
73
|
|
|
68
74
|
def _two_step_runner(
|
|
69
|
-
all_calls: list[list[str]],
|
|
75
|
+
all_calls: list[list[str]],
|
|
76
|
+
guidance: str = "PLAN\ninspect",
|
|
77
|
+
all_call_keywords: list[dict[str, object]] | None = None,
|
|
70
78
|
) -> _ProcessRunner:
|
|
71
79
|
def runner(
|
|
72
80
|
arguments: list[str], **kwargs: object
|
|
73
81
|
) -> subprocess.CompletedProcess[str]:
|
|
74
82
|
all_calls.append(arguments)
|
|
83
|
+
if all_call_keywords is not None:
|
|
84
|
+
all_call_keywords.append(kwargs)
|
|
75
85
|
if len(all_calls) == 1:
|
|
76
|
-
return
|
|
86
|
+
return _pick(NORMAL_ANSWER)
|
|
77
87
|
return subprocess.CompletedProcess(arguments, 0, _events(guidance=guidance), "")
|
|
78
88
|
return runner
|
|
79
89
|
|
|
@@ -155,31 +165,63 @@ def test_argument_parser_accepts_astra_and_rejects_sol() -> None:
|
|
|
155
165
|
parser.parse_args(["--bind", "--cwd", ".", "--enable-sol"])
|
|
156
166
|
|
|
157
167
|
|
|
158
|
-
def
|
|
159
|
-
path = astra_advisor.
|
|
160
|
-
assert path ==
|
|
168
|
+
def test_account_picker_path_names_the_packaged_picker() -> None:
|
|
169
|
+
path = astra_advisor.resolve_account_picker_path()
|
|
170
|
+
assert path == PICKER_PATH.resolve()
|
|
171
|
+
assert path.is_file()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_preflight_runs_the_picker_choose_command() -> None:
|
|
175
|
+
all_calls: list[list[str]] = []
|
|
176
|
+
|
|
177
|
+
def runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
178
|
+
all_calls.append(arguments)
|
|
179
|
+
return _pick(NORMAL_ANSWER)
|
|
161
180
|
|
|
181
|
+
astra_advisor.run_astra_preflight(PICKER_PATH, runner)
|
|
182
|
+
assert all_calls == [[sys.executable, str(PICKER_PATH), "choose"]]
|
|
162
183
|
|
|
163
|
-
|
|
184
|
+
|
|
185
|
+
def test_preflight_accepts_the_normal_tier_and_keeps_its_codex_home() -> None:
|
|
164
186
|
preflight = astra_advisor.run_astra_preflight(
|
|
165
|
-
|
|
187
|
+
PICKER_PATH, lambda arguments, **kwargs: _pick(NORMAL_ANSWER)
|
|
166
188
|
)
|
|
167
189
|
assert preflight.eligible
|
|
168
190
|
assert preflight.percent_left == 90
|
|
191
|
+
assert preflight.codex_home == PICKED_CODEX_HOME
|
|
169
192
|
|
|
170
193
|
|
|
171
|
-
|
|
194
|
+
@pytest.mark.parametrize("tier", ["luna", "wait"])
|
|
195
|
+
def test_preflight_declines_a_tier_without_room(tier: str) -> None:
|
|
196
|
+
answer = {**NORMAL_ANSWER, "tier": tier, "reason": "every account is low"}
|
|
172
197
|
preflight = astra_advisor.run_astra_preflight(
|
|
173
|
-
|
|
198
|
+
PICKER_PATH, lambda arguments, **kwargs: _pick(answer)
|
|
174
199
|
)
|
|
175
200
|
assert not preflight.eligible
|
|
176
201
|
assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_DECLINED
|
|
202
|
+
assert preflight.codex_home is None
|
|
177
203
|
|
|
178
204
|
|
|
179
|
-
@pytest.mark.parametrize(
|
|
180
|
-
|
|
205
|
+
@pytest.mark.parametrize(
|
|
206
|
+
"payload",
|
|
207
|
+
[
|
|
208
|
+
{"tier": None},
|
|
209
|
+
{**NORMAL_ANSWER, "codex_home": None},
|
|
210
|
+
{**NORMAL_ANSWER, "percent_left": "90"},
|
|
211
|
+
["bad"],
|
|
212
|
+
],
|
|
213
|
+
)
|
|
214
|
+
def test_preflight_rejects_a_malformed_answer(payload: object) -> None:
|
|
215
|
+
preflight = astra_advisor.run_astra_preflight(
|
|
216
|
+
PICKER_PATH, lambda arguments, **kwargs: _pick(payload)
|
|
217
|
+
)
|
|
218
|
+
assert not preflight.eligible
|
|
219
|
+
assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_preflight_rejects_a_failed_picker() -> None:
|
|
181
223
|
preflight = astra_advisor.run_astra_preflight(
|
|
182
|
-
|
|
224
|
+
PICKER_PATH, lambda arguments, **kwargs: _pick(NORMAL_ANSWER, returncode=1)
|
|
183
225
|
)
|
|
184
226
|
assert not preflight.eligible
|
|
185
227
|
assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
|
|
@@ -208,17 +250,21 @@ def test_jsonl_parser_falls_back_for_invalid_reply(jsonl_text: str) -> None:
|
|
|
208
250
|
assert not reply.successful
|
|
209
251
|
|
|
210
252
|
|
|
211
|
-
def
|
|
253
|
+
def test_bind_runs_picker_then_codex_under_the_chosen_home() -> None:
|
|
212
254
|
calls: list[list[str]] = []
|
|
255
|
+
all_call_keywords: list[dict[str, object]] = []
|
|
213
256
|
reply = astra_advisor.run_codex_astra_advisor(
|
|
214
257
|
"consult",
|
|
215
258
|
Path("."),
|
|
216
259
|
None,
|
|
217
|
-
|
|
260
|
+
PICKER_PATH,
|
|
218
261
|
ENABLED_SETTINGS,
|
|
219
262
|
None,
|
|
220
|
-
_two_step_runner(calls),
|
|
263
|
+
_two_step_runner(calls, all_call_keywords=all_call_keywords),
|
|
221
264
|
)
|
|
265
|
+
codex_environment = all_call_keywords[1]["env"]
|
|
266
|
+
assert isinstance(codex_environment, dict)
|
|
267
|
+
assert codex_environment["CODEX_HOME"] == str(PICKED_CODEX_HOME)
|
|
222
268
|
assert reply.successful
|
|
223
269
|
assert reply.selected_tier == "Astra"
|
|
224
270
|
assert reply.outcome == "codex"
|
|
@@ -236,7 +282,7 @@ def test_bind_routes_astra_xhigh_to_the_policy_effort() -> None:
|
|
|
236
282
|
"consult",
|
|
237
283
|
Path("."),
|
|
238
284
|
None,
|
|
239
|
-
|
|
285
|
+
PICKER_PATH,
|
|
240
286
|
settings,
|
|
241
287
|
None,
|
|
242
288
|
_two_step_runner(calls),
|
|
@@ -263,7 +309,7 @@ def test_disabled_flag_returns_declined_fallback() -> None:
|
|
|
263
309
|
def test_missing_codex_returns_broken_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
264
310
|
monkeypatch.setattr(astra_advisor.shutil, "which", lambda name: None)
|
|
265
311
|
reply = astra_advisor.run_codex_astra_advisor(
|
|
266
|
-
"consult", Path("."), None,
|
|
312
|
+
"consult", Path("."), None, PICKER_PATH, ENABLED_SETTINGS, None, subprocess.run
|
|
267
313
|
)
|
|
268
314
|
assert reply.is_fallback
|
|
269
315
|
assert reply.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
|
|
@@ -275,7 +321,7 @@ def test_resume_uses_existing_session() -> None:
|
|
|
275
321
|
"resume",
|
|
276
322
|
Path("."),
|
|
277
323
|
None,
|
|
278
|
-
|
|
324
|
+
PICKER_PATH,
|
|
279
325
|
ENABLED_SETTINGS,
|
|
280
326
|
"thread-1",
|
|
281
327
|
_two_step_runner(calls, "ENDORSE\nready"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for Codex Astra
|
|
1
|
+
"""Tests for the Codex Astra account preflight."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import subprocess
|
|
@@ -12,13 +12,13 @@ sys.path.insert(0, str(SCRIPTS_ROOT))
|
|
|
12
12
|
from codex_astra_preflight import run_astra_preflight
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
|
|
15
|
+
def test_run_astra_preflight_rejects_picker_failure(tmp_path: Path) -> None:
|
|
16
|
+
picker_path = tmp_path / "codex_account_choice.py"
|
|
17
17
|
|
|
18
18
|
def process_runner(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
19
19
|
return subprocess.CompletedProcess([], 1, json.dumps({}), "")
|
|
20
20
|
|
|
21
|
-
preflight = run_astra_preflight(
|
|
21
|
+
preflight = run_astra_preflight(picker_path, process_runner)
|
|
22
22
|
|
|
23
23
|
assert not preflight.eligible
|
|
24
24
|
assert preflight.fallback_kind == "broken"
|