claude-dev-env 8.38.3 → 8.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/scripts/codex_astra_advisor.py +13 -13
- package/_shared/advisor/scripts/codex_astra_preflight.py +62 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +5 -2
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +64 -33
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +125 -3
- package/_shared/pr-loop/scripts/check_convergence.py +23 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +4 -3
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +64 -25
- package/docs/codex-accounts.md +27 -31
- package/package.json +1 -1
- package/scripts/codex_account_choice.py +8 -303
- package/scripts/test_codex_account_choice.py +11 -273
|
@@ -14,26 +14,26 @@ A Windows `setx` write only updates the persisted user environment; only a proce
|
|
|
14
14
|
|
|
15
15
|
`ADVISOR_EFFORT` selects Codex `model_reasoning_effort` for Astra: `low`, `medium`, `high`, `xhigh`, or `max`. The policy default is `medium`, which sends `model_reasoning_effort="medium"`. Astra Xhigh and Max requests route to Medium. Pass `--effort <level>` on the helper to set effort for that Astra run without changing the environment. An unset value uses the policy default. An unknown value blocks advisor routing.
|
|
16
16
|
|
|
17
|
-
Every fallback reply carries a `fallback_kind` field. `declined` means policy closed the rung (flag off, or the account
|
|
17
|
+
Every fallback reply carries a `fallback_kind` field. `declined` means policy closed the rung (flag off, or the account broker named no Codex account on the `normal` tier). `broken` means the Astra path itself failed (missing executable, spawn error, timeout, malformed reply). A `broken` fallback is a defect to report.
|
|
18
18
|
|
|
19
19
|
## Preflight
|
|
20
20
|
|
|
21
|
-
Flag on: the helper asks the
|
|
21
|
+
Flag on: the helper asks the account broker for a Codex account first.
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
python ~/.claude/scripts/
|
|
24
|
+
python ~/.claude/scripts/account_broker.py choose --product codex
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
**Broker path.** `codex_astra_advisor.resolve_account_broker_path` builds the broker path from the helper's own location: two directories above `_shared/advisor/scripts`, then `scripts/account_broker.py`. Installed, that is `~/.claude/scripts/account_broker.py`. Repo home: `packages/claude-dev-env/scripts/`. The accounts, their order, and sign-in steps are in [`codex-accounts.md`](../../../docs/codex-accounts.md).
|
|
28
28
|
|
|
29
|
-
The shared entry point is `~/.claude/_shared/advisor/scripts/codex_astra_advisor.py`. It runs the
|
|
29
|
+
The shared entry point is `~/.claude/_shared/advisor/scripts/codex_astra_advisor.py`. It runs the broker and owns Astra bind or resume parsing. Bind with `python ~/.claude/_shared/advisor/scripts/codex_astra_advisor.py --bind --enable-astra --cwd <repo-root>` and pipe the charter on stdin. Resume with `--resume <session_id>` and pipe the delta consult on stdin.
|
|
30
30
|
|
|
31
|
-
The gate passes only when the
|
|
31
|
+
The gate passes only when the broker exits 0 and answers `decision.tier: normal` with a `decision.home` and finite session or weekly room in the `accounts` entry whose `name` and `home` match. With no roster configured, `accounts` is empty and the gate passes on `decision.home` alone. The helper then runs Codex with `CODEX_HOME` set to `decision.home`. A `luna` or `wait` answer, including exit 3 with a wait answer, declines the preflight when Opus did not bind.
|
|
32
32
|
|
|
33
33
|
## Branches
|
|
34
34
|
|
|
35
35
|
**Preflight pass.** Bind one Codex CLI session at `gpt-6-astra` with `model_reasoning_effort` set from the policy-selected `ADVISOR_EFFORT` value, `--sandbox read-only`, and JSON output. The helper receives the standing-reviewer charter on stdin and returns only parsed ENDORSE / CORRECTION / PLAN / STOP guidance with a session ID.
|
|
36
36
|
|
|
37
|
-
**Preflight fail.** A
|
|
37
|
+
**Preflight fail.** A broker failure other than exit 3 with a wait answer, timeout, malformed answer, or a `luna` or `wait` tier fails closed when Opus did not bind.
|
|
38
38
|
|
|
39
39
|
The helper owns the Astra attempt and returns an explicit fallback result. The consuming advisor path owns the Opus bind. Apply the same gate to every Astra attempt, including resume.
|
|
@@ -30,7 +30,7 @@ from advisor_scripts_constants.advisor_route_constants import (
|
|
|
30
30
|
)
|
|
31
31
|
from advisor_scripts_constants.astra_advisor_constants import (
|
|
32
32
|
ADVISOR_CODEX_EXECUTABLE_ENV_VAR,
|
|
33
|
-
|
|
33
|
+
ALL_ACCOUNT_BROKER_RELATIVE_PARTS,
|
|
34
34
|
ALL_ASTRA_TRUTHY_VALUES,
|
|
35
35
|
ASTRA_BIND_FAILURE_REASON,
|
|
36
36
|
ASTRA_CODEX_TIMEOUT_REASON,
|
|
@@ -174,18 +174,18 @@ def resolve_advisor_effort(all_settings: Mapping[str, str] | None) -> str:
|
|
|
174
174
|
return resolve_advisor_pair(all_settings, None)[1]
|
|
175
175
|
|
|
176
176
|
|
|
177
|
-
def
|
|
178
|
-
"""Return the
|
|
177
|
+
def resolve_account_broker_path() -> Path:
|
|
178
|
+
"""Return the account broker in the scripts directory beside this shared tree.
|
|
179
179
|
|
|
180
180
|
::
|
|
181
181
|
|
|
182
|
-
~/.claude/_shared/advisor/scripts -> ~/.claude/scripts/
|
|
182
|
+
~/.claude/_shared/advisor/scripts -> ~/.claude/scripts/account_broker.py
|
|
183
183
|
|
|
184
184
|
Returns:
|
|
185
|
-
Path to ``
|
|
185
|
+
Path to ``account_broker.py``.
|
|
186
186
|
"""
|
|
187
187
|
shared_root = _scripts_directory.parents[SHARED_PACKAGE_ROOT_PARENT_INDEX]
|
|
188
|
-
return shared_root.joinpath(*
|
|
188
|
+
return shared_root.joinpath(*ALL_ACCOUNT_BROKER_RELATIVE_PARTS)
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
def resolve_codex_executable(all_settings: Mapping[str, str] | None) -> str | None:
|
|
@@ -237,12 +237,12 @@ def build_codex_arguments(
|
|
|
237
237
|
|
|
238
238
|
def _resolve_preflight(
|
|
239
239
|
preflight: AstraPreflight | None,
|
|
240
|
-
|
|
240
|
+
broker_path: Path | None,
|
|
241
241
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
242
242
|
) -> AstraPreflight:
|
|
243
243
|
if preflight is not None:
|
|
244
244
|
return preflight
|
|
245
|
-
resolved_path =
|
|
245
|
+
resolved_path = resolve_account_broker_path() if broker_path is None else broker_path
|
|
246
246
|
return run_astra_preflight(resolved_path, process_runner)
|
|
247
247
|
|
|
248
248
|
|
|
@@ -331,7 +331,7 @@ def run_codex_astra_advisor(
|
|
|
331
331
|
prompt: str,
|
|
332
332
|
working_directory: Path,
|
|
333
333
|
preflight: AstraPreflight | None,
|
|
334
|
-
|
|
334
|
+
broker_path: Path | None,
|
|
335
335
|
setting_by_name: Mapping[str, str] | None,
|
|
336
336
|
session_id: str | None,
|
|
337
337
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
@@ -341,11 +341,11 @@ def run_codex_astra_advisor(
|
|
|
341
341
|
Args:
|
|
342
342
|
prompt: Advisor prompt text.
|
|
343
343
|
working_directory: Working directory for Codex.
|
|
344
|
-
preflight: Account
|
|
345
|
-
|
|
344
|
+
preflight: Account broker result, or None to run the broker.
|
|
345
|
+
broker_path: Account broker path, or None for the one beside this shared tree.
|
|
346
346
|
setting_by_name: Environment mapping, or None to read os.environ.
|
|
347
347
|
session_id: Session to resume, or None for a new bind.
|
|
348
|
-
process_runner: Callable that runs the
|
|
348
|
+
process_runner: Callable that runs the broker and Codex.
|
|
349
349
|
|
|
350
350
|
Returns:
|
|
351
351
|
Advisor reply, or a fallback.
|
|
@@ -355,7 +355,7 @@ def run_codex_astra_advisor(
|
|
|
355
355
|
executable = resolve_codex_executable(setting_by_name)
|
|
356
356
|
if executable is None:
|
|
357
357
|
return build_fallback_reply(ASTRA_EXECUTABLE_NOT_FOUND_REASON, True)
|
|
358
|
-
resolved_preflight = _resolve_preflight(preflight,
|
|
358
|
+
resolved_preflight = _resolve_preflight(preflight, broker_path, process_runner)
|
|
359
359
|
return _run_enabled_advisor(prompt, working_directory, setting_by_name, session_id, executable, resolved_preflight, process_runner)
|
|
360
360
|
|
|
361
361
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Ask the
|
|
1
|
+
"""Ask the account broker for a Codex account before an Astra advisor request."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -11,9 +11,12 @@ from dataclasses import dataclass
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from advisor_scripts_constants.astra_advisor_constants import (
|
|
14
|
-
|
|
14
|
+
ACCOUNT_BROKER_CHOOSE_COMMAND,
|
|
15
15
|
ASTRA_ACCOUNT_PICK_TIMEOUT_REASON,
|
|
16
|
+
ALL_BROKER_ACCEPTED_EXIT_CODES,
|
|
17
|
+
ALL_BROKER_METER_PERCENT_KEYS,
|
|
16
18
|
ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS,
|
|
19
|
+
BROKER_WAIT_EXIT_CODE,
|
|
17
20
|
ASTRA_FALLBACK_KIND_BROKEN,
|
|
18
21
|
ASTRA_FALLBACK_KIND_DECLINED,
|
|
19
22
|
ASTRA_PREFLIGHT_FAILURE_REASON,
|
|
@@ -49,25 +52,51 @@ def _finite_percent(raw_percent: object) -> float | None:
|
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def _preflight_from_answer(field_by_name: dict[str, object]) -> AstraPreflight:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
decision = field_by_name.get("decision")
|
|
56
|
+
if not isinstance(decision, dict) or not isinstance(decision.get("tier"), str):
|
|
57
|
+
return _broken("broker answer is malformed")
|
|
58
|
+
if decision["tier"] != CODEX_TIER_NORMAL:
|
|
59
|
+
reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: no Codex account has room ({decision.get('reason')})"
|
|
56
60
|
return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_DECLINED)
|
|
57
|
-
codex_home =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
codex_home = decision.get("home")
|
|
62
|
+
account = decision.get("account")
|
|
63
|
+
accounts = field_by_name.get("accounts")
|
|
64
|
+
reason = decision.get("reason")
|
|
65
|
+
if not isinstance(codex_home, str) or not codex_home or not isinstance(account, str) or not isinstance(accounts, list):
|
|
66
|
+
return _broken("broker answer names no Codex home with room")
|
|
67
|
+
if not isinstance(reason, str):
|
|
68
|
+
return _broken("broker answer is malformed")
|
|
69
|
+
if not accounts:
|
|
70
|
+
return AstraPreflight(True, None, reason, codex_home=Path(codex_home))
|
|
71
|
+
chosen = next(
|
|
72
|
+
(
|
|
73
|
+
entry
|
|
74
|
+
for entry in accounts
|
|
75
|
+
if isinstance(entry, dict)
|
|
76
|
+
and entry.get("name") == account
|
|
77
|
+
and entry.get("home") == codex_home
|
|
78
|
+
),
|
|
79
|
+
None,
|
|
80
|
+
)
|
|
81
|
+
meters = chosen.get("meters") if isinstance(chosen, dict) else None
|
|
82
|
+
if not isinstance(meters, dict):
|
|
83
|
+
return _broken("broker answer names no Codex home with room")
|
|
84
|
+
all_percent_left = [
|
|
85
|
+
percent
|
|
86
|
+
for key in ALL_BROKER_METER_PERCENT_KEYS
|
|
87
|
+
if (percent := _finite_percent(meters.get(key))) is not None
|
|
88
|
+
]
|
|
89
|
+
if not all_percent_left:
|
|
90
|
+
return _broken("broker answer names no Codex home with room")
|
|
91
|
+
return AstraPreflight(True, min(all_percent_left), reason, codex_home=Path(codex_home))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _run_broker(
|
|
95
|
+
broker_path: Path,
|
|
67
96
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
68
97
|
) -> subprocess.CompletedProcess[str]:
|
|
69
98
|
return process_runner(
|
|
70
|
-
[sys.executable, str(
|
|
99
|
+
[sys.executable, str(broker_path), ACCOUNT_BROKER_CHOOSE_COMMAND, "--product", "codex"],
|
|
71
100
|
capture_output=True,
|
|
72
101
|
text=True,
|
|
73
102
|
check=False,
|
|
@@ -76,39 +105,39 @@ def _run_picker(
|
|
|
76
105
|
)
|
|
77
106
|
|
|
78
107
|
|
|
79
|
-
def
|
|
80
|
-
if completed.returncode
|
|
81
|
-
return _broken(f"
|
|
108
|
+
def _preflight_from_broker(completed: subprocess.CompletedProcess[str]) -> AstraPreflight:
|
|
109
|
+
if completed.returncode not in ALL_BROKER_ACCEPTED_EXIT_CODES:
|
|
110
|
+
return _broken(f"broker exit {completed.returncode}")
|
|
82
111
|
try:
|
|
83
112
|
answer = json.loads(completed.stdout)
|
|
84
113
|
except (TypeError, json.JSONDecodeError):
|
|
85
|
-
return _broken("
|
|
114
|
+
return _broken("broker answer is malformed")
|
|
86
115
|
if not isinstance(answer, dict):
|
|
87
|
-
return _broken("
|
|
116
|
+
return _broken("broker answer is malformed")
|
|
117
|
+
if completed.returncode == BROKER_WAIT_EXIT_CODE:
|
|
118
|
+
decision = answer.get("decision")
|
|
119
|
+
if not isinstance(decision, dict) or not isinstance(decision.get("tier"), str):
|
|
120
|
+
return _broken("broker answer is malformed")
|
|
121
|
+
reason = f"{ASTRA_PREFLIGHT_FAILURE_REASON}: no Codex account has room ({decision.get('reason')})"
|
|
122
|
+
return _preflight_fallback(reason, None, ASTRA_FALLBACK_KIND_DECLINED)
|
|
88
123
|
return _preflight_from_answer(answer)
|
|
89
124
|
|
|
90
125
|
|
|
91
126
|
def run_astra_preflight(
|
|
92
|
-
|
|
127
|
+
broker_path: Path,
|
|
93
128
|
process_runner: Callable[..., subprocess.CompletedProcess[str]],
|
|
94
129
|
) -> AstraPreflight:
|
|
95
|
-
"""Run the Codex account
|
|
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
|
|
130
|
+
"""Run the Codex account broker and decide whether Astra may bind.
|
|
102
131
|
|
|
103
132
|
Args:
|
|
104
|
-
|
|
105
|
-
process_runner: Callable that runs the
|
|
133
|
+
broker_path: Path to ``account_broker.py``.
|
|
134
|
+
process_runner: Callable that runs the broker.
|
|
106
135
|
|
|
107
136
|
Returns:
|
|
108
137
|
Eligibility, the chosen account's percent left and Codex home, reason, and fallback kind.
|
|
109
138
|
"""
|
|
110
139
|
try:
|
|
111
|
-
return
|
|
140
|
+
return _preflight_from_broker(_run_broker(broker_path, process_runner))
|
|
112
141
|
except subprocess.TimeoutExpired as error:
|
|
113
142
|
return _preflight_fallback(
|
|
114
143
|
f"{ASTRA_ACCOUNT_PICK_TIMEOUT_REASON}: {error}", None, ASTRA_FALLBACK_KIND_BROKEN
|
|
@@ -14,10 +14,13 @@ CODEX_EXEC_SUBCOMMAND: str = "exec"
|
|
|
14
14
|
CODEX_RESUME_SUBCOMMAND: str = "resume"
|
|
15
15
|
CODEX_SANDBOX_FLAG: str = "--sandbox"
|
|
16
16
|
CODEX_HOME_ENV_VAR: str = "CODEX_HOME"
|
|
17
|
-
|
|
17
|
+
ALL_ACCOUNT_BROKER_RELATIVE_PARTS: tuple[str, ...] = ("scripts", "account_broker.py")
|
|
18
18
|
SHARED_PACKAGE_ROOT_PARENT_INDEX: int = 2
|
|
19
|
-
|
|
19
|
+
ACCOUNT_BROKER_CHOOSE_COMMAND: str = "choose"
|
|
20
20
|
CODEX_TIER_NORMAL: str = "normal"
|
|
21
|
+
BROKER_WAIT_EXIT_CODE: int = 3
|
|
22
|
+
ALL_BROKER_ACCEPTED_EXIT_CODES: tuple[int, ...] = (0, BROKER_WAIT_EXIT_CODE)
|
|
23
|
+
ALL_BROKER_METER_PERCENT_KEYS: tuple[str, ...] = ("session_percent_left", "weekly_percent_left")
|
|
21
24
|
ASTRA_SESSION_ID_METAVAR: str = "SESSION_ID"
|
|
22
25
|
ASTRA_CODEX_TIMEOUT_SECONDS: float = 120.0
|
|
23
26
|
ASTRA_ACCOUNT_PICK_TIMEOUT_SECONDS: float = 150.0
|
|
@@ -31,14 +31,28 @@ def _load_astra_module() -> ModuleType:
|
|
|
31
31
|
astra_advisor = _load_astra_module()
|
|
32
32
|
|
|
33
33
|
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
34
|
-
|
|
34
|
+
BROKER_PATH = SCRIPTS_ROOT.parents[2] / "scripts" / "account_broker.py"
|
|
35
35
|
PICKED_CODEX_HOME = Path("codex-profiles") / "codex-2"
|
|
36
36
|
NORMAL_ANSWER = {
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
"decision": {
|
|
38
|
+
"action": "run",
|
|
39
|
+
"tier": "normal",
|
|
40
|
+
"account": "codex-2",
|
|
41
|
+
"home": str(PICKED_CODEX_HOME),
|
|
42
|
+
"resets_at": None,
|
|
43
|
+
"reason": "codex-2 has 90% left",
|
|
44
|
+
},
|
|
45
|
+
"accounts": [{
|
|
46
|
+
"name": "codex-2",
|
|
47
|
+
"home": str(PICKED_CODEX_HOME),
|
|
48
|
+
"is_main": False,
|
|
49
|
+
"meters": {
|
|
50
|
+
"session_percent_left": 90,
|
|
51
|
+
"session_resets_at": None,
|
|
52
|
+
"weekly_percent_left": 95,
|
|
53
|
+
"weekly_resets_at": None,
|
|
54
|
+
},
|
|
55
|
+
}],
|
|
42
56
|
}
|
|
43
57
|
ENABLED_SETTINGS = {astra_advisor.ASTRA_ENV_VAR: "1"}
|
|
44
58
|
|
|
@@ -48,9 +62,9 @@ def _codex_on_search_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
48
62
|
monkeypatch.setattr(astra_advisor.shutil, "which", lambda name: "codex")
|
|
49
63
|
|
|
50
64
|
|
|
51
|
-
def
|
|
65
|
+
def _broker_answer(payload: object, returncode: int = 0) -> subprocess.CompletedProcess[str]:
|
|
52
66
|
return subprocess.CompletedProcess(
|
|
53
|
-
[sys.executable, str(
|
|
67
|
+
[sys.executable, str(BROKER_PATH), "choose", "--product", "codex"],
|
|
54
68
|
returncode,
|
|
55
69
|
json.dumps(payload),
|
|
56
70
|
"",
|
|
@@ -83,7 +97,7 @@ def _two_step_runner(
|
|
|
83
97
|
if all_call_keywords is not None:
|
|
84
98
|
all_call_keywords.append(kwargs)
|
|
85
99
|
if len(all_calls) == 1:
|
|
86
|
-
return
|
|
100
|
+
return _broker_answer(NORMAL_ANSWER)
|
|
87
101
|
return subprocess.CompletedProcess(arguments, 0, _events(guidance=guidance), "")
|
|
88
102
|
return runner
|
|
89
103
|
|
|
@@ -165,26 +179,26 @@ def test_argument_parser_accepts_astra_and_rejects_sol() -> None:
|
|
|
165
179
|
parser.parse_args(["--bind", "--cwd", ".", "--enable-sol"])
|
|
166
180
|
|
|
167
181
|
|
|
168
|
-
def
|
|
169
|
-
path = astra_advisor.
|
|
170
|
-
assert path ==
|
|
182
|
+
def test_should_resolve_packaged_account_broker_path() -> None:
|
|
183
|
+
path = astra_advisor.resolve_account_broker_path()
|
|
184
|
+
assert path == BROKER_PATH.resolve()
|
|
171
185
|
assert path.is_file()
|
|
172
186
|
|
|
173
187
|
|
|
174
|
-
def
|
|
188
|
+
def test_should_run_broker_choose_command_for_codex_preflight() -> None:
|
|
175
189
|
all_calls: list[list[str]] = []
|
|
176
190
|
|
|
177
191
|
def runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
178
192
|
all_calls.append(arguments)
|
|
179
|
-
return
|
|
193
|
+
return _broker_answer(NORMAL_ANSWER)
|
|
180
194
|
|
|
181
|
-
astra_advisor.run_astra_preflight(
|
|
182
|
-
assert all_calls == [[sys.executable, str(
|
|
195
|
+
astra_advisor.run_astra_preflight(BROKER_PATH, runner)
|
|
196
|
+
assert all_calls == [[sys.executable, str(BROKER_PATH), "choose", "--product", "codex"]]
|
|
183
197
|
|
|
184
198
|
|
|
185
|
-
def
|
|
199
|
+
def test_should_accept_normal_tier_and_keep_its_codex_home() -> None:
|
|
186
200
|
preflight = astra_advisor.run_astra_preflight(
|
|
187
|
-
|
|
201
|
+
BROKER_PATH, lambda arguments, **kwargs: _broker_answer(NORMAL_ANSWER)
|
|
188
202
|
)
|
|
189
203
|
assert preflight.eligible
|
|
190
204
|
assert preflight.percent_left == 90
|
|
@@ -192,10 +206,13 @@ def test_preflight_accepts_the_normal_tier_and_keeps_its_codex_home() -> None:
|
|
|
192
206
|
|
|
193
207
|
|
|
194
208
|
@pytest.mark.parametrize("tier", ["luna", "wait"])
|
|
195
|
-
def
|
|
196
|
-
answer = {
|
|
209
|
+
def test_should_decline_a_tier_without_room(tier: str) -> None:
|
|
210
|
+
answer = {
|
|
211
|
+
**NORMAL_ANSWER,
|
|
212
|
+
"decision": {**NORMAL_ANSWER["decision"], "tier": tier, "reason": "every account is low"},
|
|
213
|
+
}
|
|
197
214
|
preflight = astra_advisor.run_astra_preflight(
|
|
198
|
-
|
|
215
|
+
BROKER_PATH, lambda arguments, **kwargs: _broker_answer(answer)
|
|
199
216
|
)
|
|
200
217
|
assert not preflight.eligible
|
|
201
218
|
assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_DECLINED
|
|
@@ -205,23 +222,37 @@ def test_preflight_declines_a_tier_without_room(tier: str) -> None:
|
|
|
205
222
|
@pytest.mark.parametrize(
|
|
206
223
|
"payload",
|
|
207
224
|
[
|
|
208
|
-
{"tier": None},
|
|
209
|
-
{**NORMAL_ANSWER, "
|
|
210
|
-
{
|
|
225
|
+
{"decision": {"tier": None}},
|
|
226
|
+
{**NORMAL_ANSWER, "decision": {**NORMAL_ANSWER["decision"], "home": None}},
|
|
227
|
+
{
|
|
228
|
+
**NORMAL_ANSWER,
|
|
229
|
+
"accounts": [{
|
|
230
|
+
"name": "codex-2",
|
|
231
|
+
"home": str(PICKED_CODEX_HOME),
|
|
232
|
+
"meters": {"session_percent_left": "90"},
|
|
233
|
+
}],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
**NORMAL_ANSWER,
|
|
237
|
+
"accounts": [{
|
|
238
|
+
**NORMAL_ANSWER["accounts"][0],
|
|
239
|
+
"home": "codex-profiles/codex-1",
|
|
240
|
+
}],
|
|
241
|
+
},
|
|
211
242
|
["bad"],
|
|
212
243
|
],
|
|
213
244
|
)
|
|
214
|
-
def
|
|
245
|
+
def test_should_reject_a_malformed_broker_answer(payload: object) -> None:
|
|
215
246
|
preflight = astra_advisor.run_astra_preflight(
|
|
216
|
-
|
|
247
|
+
BROKER_PATH, lambda arguments, **kwargs: _broker_answer(payload)
|
|
217
248
|
)
|
|
218
249
|
assert not preflight.eligible
|
|
219
250
|
assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
|
|
220
251
|
|
|
221
252
|
|
|
222
|
-
def
|
|
253
|
+
def test_should_reject_failed_broker_preflight() -> None:
|
|
223
254
|
preflight = astra_advisor.run_astra_preflight(
|
|
224
|
-
|
|
255
|
+
BROKER_PATH, lambda arguments, **kwargs: _broker_answer(NORMAL_ANSWER, returncode=1)
|
|
225
256
|
)
|
|
226
257
|
assert not preflight.eligible
|
|
227
258
|
assert preflight.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
|
|
@@ -250,14 +281,14 @@ def test_jsonl_parser_falls_back_for_invalid_reply(jsonl_text: str) -> None:
|
|
|
250
281
|
assert not reply.successful
|
|
251
282
|
|
|
252
283
|
|
|
253
|
-
def
|
|
284
|
+
def test_should_bind_under_the_broker_chosen_home() -> None:
|
|
254
285
|
calls: list[list[str]] = []
|
|
255
286
|
all_call_keywords: list[dict[str, object]] = []
|
|
256
287
|
reply = astra_advisor.run_codex_astra_advisor(
|
|
257
288
|
"consult",
|
|
258
289
|
Path("."),
|
|
259
290
|
None,
|
|
260
|
-
|
|
291
|
+
BROKER_PATH,
|
|
261
292
|
ENABLED_SETTINGS,
|
|
262
293
|
None,
|
|
263
294
|
_two_step_runner(calls, all_call_keywords=all_call_keywords),
|
|
@@ -282,7 +313,7 @@ def test_bind_routes_astra_xhigh_to_the_policy_effort() -> None:
|
|
|
282
313
|
"consult",
|
|
283
314
|
Path("."),
|
|
284
315
|
None,
|
|
285
|
-
|
|
316
|
+
BROKER_PATH,
|
|
286
317
|
settings,
|
|
287
318
|
None,
|
|
288
319
|
_two_step_runner(calls),
|
|
@@ -309,7 +340,7 @@ def test_disabled_flag_returns_declined_fallback() -> None:
|
|
|
309
340
|
def test_missing_codex_returns_broken_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
310
341
|
monkeypatch.setattr(astra_advisor.shutil, "which", lambda name: None)
|
|
311
342
|
reply = astra_advisor.run_codex_astra_advisor(
|
|
312
|
-
"consult", Path("."), None,
|
|
343
|
+
"consult", Path("."), None, BROKER_PATH, ENABLED_SETTINGS, None, subprocess.run
|
|
313
344
|
)
|
|
314
345
|
assert reply.is_fallback
|
|
315
346
|
assert reply.fallback_kind == astra_advisor.ASTRA_FALLBACK_KIND_BROKEN
|
|
@@ -321,7 +352,7 @@ def test_resume_uses_existing_session() -> None:
|
|
|
321
352
|
"resume",
|
|
322
353
|
Path("."),
|
|
323
354
|
None,
|
|
324
|
-
|
|
355
|
+
BROKER_PATH,
|
|
325
356
|
ENABLED_SETTINGS,
|
|
326
357
|
"thread-1",
|
|
327
358
|
_two_step_runner(calls, "ENDORSE\nready"),
|
|
@@ -3,22 +3,144 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
8
11
|
SCRIPTS_ROOT = Path(__file__).parent.parent
|
|
9
12
|
sys.path.insert(0, str(SCRIPTS_ROOT / "config"))
|
|
10
13
|
sys.path.insert(0, str(SCRIPTS_ROOT))
|
|
14
|
+
sys.path.insert(0, str(SCRIPTS_ROOT.parents[2] / "scripts"))
|
|
11
15
|
|
|
16
|
+
from account_broker import choose_from_readings, decision_payload, readings_payload
|
|
12
17
|
from codex_astra_preflight import run_astra_preflight
|
|
18
|
+
from dev_env_scripts_constants.account_broker_constants import (
|
|
19
|
+
Account,
|
|
20
|
+
Decision,
|
|
21
|
+
Meters,
|
|
22
|
+
Product,
|
|
23
|
+
Reading,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_accept_broker_normal_answer_and_use_tighter_meter(tmp_path: Path) -> None:
|
|
28
|
+
broker_path = tmp_path / "account_broker.py"
|
|
29
|
+
home = tmp_path / "codex-2"
|
|
30
|
+
answer = {
|
|
31
|
+
"decision": {
|
|
32
|
+
"action": "run",
|
|
33
|
+
"account": "codex-2",
|
|
34
|
+
"home": str(home),
|
|
35
|
+
"resets_at": None,
|
|
36
|
+
"reason": "codex-2 has 42% left",
|
|
37
|
+
"tier": "normal",
|
|
38
|
+
},
|
|
39
|
+
"accounts": [
|
|
40
|
+
{
|
|
41
|
+
"name": "codex-2",
|
|
42
|
+
"home": str(home),
|
|
43
|
+
"is_main": False,
|
|
44
|
+
"meters": {
|
|
45
|
+
"session_percent_left": 72,
|
|
46
|
+
"session_resets_at": None,
|
|
47
|
+
"weekly_percent_left": 42,
|
|
48
|
+
"weekly_resets_at": None,
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
calls: list[list[str]] = []
|
|
54
|
+
|
|
55
|
+
def process_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
56
|
+
calls.append(arguments)
|
|
57
|
+
return subprocess.CompletedProcess(arguments, 0, json.dumps(answer), "")
|
|
58
|
+
|
|
59
|
+
preflight = run_astra_preflight(broker_path, process_runner)
|
|
60
|
+
|
|
61
|
+
assert calls == [[sys.executable, str(broker_path), "choose", "--product", "codex"]]
|
|
62
|
+
assert preflight.eligible
|
|
63
|
+
assert preflight.percent_left == 42
|
|
64
|
+
assert preflight.codex_home == home
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_should_decline_broker_wait_answer_on_exit_three(tmp_path: Path) -> None:
|
|
68
|
+
broker_path = tmp_path / "account_broker.py"
|
|
69
|
+
answer = {
|
|
70
|
+
"decision": {
|
|
71
|
+
"action": "wait",
|
|
72
|
+
"account": None,
|
|
73
|
+
"home": None,
|
|
74
|
+
"resets_at": None,
|
|
75
|
+
"reason": "no account has room",
|
|
76
|
+
"tier": "wait",
|
|
77
|
+
},
|
|
78
|
+
"accounts": [],
|
|
79
|
+
}
|
|
13
80
|
|
|
81
|
+
def process_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
82
|
+
return subprocess.CompletedProcess(arguments, 3, json.dumps(answer), "")
|
|
14
83
|
|
|
15
|
-
|
|
16
|
-
|
|
84
|
+
preflight = run_astra_preflight(broker_path, process_runner)
|
|
85
|
+
|
|
86
|
+
assert not preflight.eligible
|
|
87
|
+
assert preflight.fallback_kind == "declined"
|
|
88
|
+
assert "no account has room" in preflight.reason
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_should_reject_malformed_broker_answer_on_exit_three(tmp_path: Path) -> None:
|
|
92
|
+
broker_path = tmp_path / "account_broker.py"
|
|
93
|
+
|
|
94
|
+
def process_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
95
|
+
return subprocess.CompletedProcess(arguments, 3, "not json", "")
|
|
96
|
+
|
|
97
|
+
preflight = run_astra_preflight(broker_path, process_runner)
|
|
98
|
+
|
|
99
|
+
assert not preflight.eligible
|
|
100
|
+
assert preflight.fallback_kind == "broken"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_should_reject_broker_failure(tmp_path: Path) -> None:
|
|
104
|
+
broker_path = tmp_path / "account_broker.py"
|
|
17
105
|
|
|
18
106
|
def process_runner(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
19
107
|
return subprocess.CompletedProcess([], 1, json.dumps({}), "")
|
|
20
108
|
|
|
21
|
-
preflight = run_astra_preflight(
|
|
109
|
+
preflight = run_astra_preflight(broker_path, process_runner)
|
|
22
110
|
|
|
23
111
|
assert not preflight.eligible
|
|
24
112
|
assert preflight.fallback_kind == "broken"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_accept_the_answer_the_broker_serializes(tmp_path: Path) -> None:
|
|
116
|
+
account = Account(Product.CODEX, "codex-2", tmp_path / "codex-2")
|
|
117
|
+
reading = Reading(account, Meters(72.0, None, 42.0, None))
|
|
118
|
+
decision = Decision("run", account, None, "codex-2 has 42% left", "normal")
|
|
119
|
+
answer = {"decision": decision_payload(decision), "accounts": readings_payload([reading])}
|
|
120
|
+
|
|
121
|
+
def process_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
122
|
+
return subprocess.CompletedProcess(arguments, 0, json.dumps(answer), "")
|
|
123
|
+
|
|
124
|
+
preflight = run_astra_preflight(tmp_path / "account_broker.py", process_runner)
|
|
125
|
+
|
|
126
|
+
assert preflight.eligible
|
|
127
|
+
assert preflight.percent_left == 42
|
|
128
|
+
assert preflight.codex_home == tmp_path / "codex-2"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_should_bind_the_default_home_when_no_roster_is_configured(
|
|
132
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
133
|
+
) -> None:
|
|
134
|
+
default_home = tmp_path / ".codex"
|
|
135
|
+
monkeypatch.setenv("CODEX_HOME", str(default_home))
|
|
136
|
+
decision = choose_from_readings(Product.CODEX, [], now=datetime.now(timezone.utc))
|
|
137
|
+
answer = {"decision": decision_payload(decision), "accounts": readings_payload([])}
|
|
138
|
+
|
|
139
|
+
def process_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
140
|
+
return subprocess.CompletedProcess(arguments, 0, json.dumps(answer), "")
|
|
141
|
+
|
|
142
|
+
preflight = run_astra_preflight(tmp_path / "account_broker.py", process_runner)
|
|
143
|
+
|
|
144
|
+
assert preflight.eligible
|
|
145
|
+
assert preflight.percent_left is None
|
|
146
|
+
assert preflight.codex_home == default_home.resolve()
|