claude-dev-env 8.46.18 → 8.46.19
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/package.json +1 -1
- package/scripts/account_broker.py +37 -13
- package/scripts/test_account_broker.py +75 -0
package/package.json
CHANGED
|
@@ -128,13 +128,22 @@ def readings_payload(all_readings: Sequence[Reading]) -> list[dict[str, object]]
|
|
|
128
128
|
]
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _stored_reset(raw: object, now: datetime) -> datetime | None:
|
|
132
|
+
if not isinstance(raw, (int, float)) or raw <= now.timestamp():
|
|
133
|
+
return None
|
|
134
|
+
try:
|
|
135
|
+
return datetime.fromtimestamp(raw, timezone.utc)
|
|
136
|
+
except (ValueError, OSError, OverflowError):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
131
140
|
def _spent_for_readings(all_readings: Sequence[Reading], all_state: dict[str, object], now: datetime) -> tuple[frozenset[Account], dict[Account, datetime]]:
|
|
132
141
|
marks = all_state["spent"]
|
|
133
142
|
spent = {}
|
|
134
143
|
for each_reading in all_readings:
|
|
135
|
-
|
|
136
|
-
if
|
|
137
|
-
spent[each_reading.account] =
|
|
144
|
+
reset = _stored_reset(marks.get(_state_key(each_reading.account)), now)
|
|
145
|
+
if reset is not None:
|
|
146
|
+
spent[each_reading.account] = reset
|
|
138
147
|
return frozenset(spent), spent
|
|
139
148
|
|
|
140
149
|
|
|
@@ -147,21 +156,31 @@ def _mark_spent(all_state: dict[str, object], reading: Reading, now: datetime, r
|
|
|
147
156
|
return until
|
|
148
157
|
|
|
149
158
|
|
|
150
|
-
def _outside_roster_resets(product: Product, all_readings: Sequence[Reading], all_state: Mapping[str, object], now: datetime) ->
|
|
159
|
+
def _outside_roster_resets(product: Product, all_readings: Sequence[Reading], all_state: Mapping[str, object], now: datetime) -> dict[str, datetime]:
|
|
151
160
|
marks = all_state.get("spent") if isinstance(all_state.get("spent"), dict) else {}
|
|
152
161
|
all_roster_names = {each_reading.account.name for each_reading in all_readings}
|
|
153
162
|
prefix = f"{product.value}:"
|
|
154
163
|
placeholder_suffix = f":{Path()}"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
all_resets_by_name = {}
|
|
165
|
+
for each_key, each_raw in marks.items():
|
|
166
|
+
if not (isinstance(each_key, str) and each_key.startswith(prefix) and each_key.endswith(placeholder_suffix)):
|
|
167
|
+
continue
|
|
168
|
+
name = each_key[len(prefix):-len(placeholder_suffix)]
|
|
169
|
+
reset = _stored_reset(each_raw, now)
|
|
170
|
+
if name not in all_roster_names and reset is not None:
|
|
171
|
+
all_resets_by_name[name] = reset
|
|
172
|
+
return all_resets_by_name
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _with_outside_spent_marks(decision: Decision, all_outside_resets_by_name: Mapping[str, datetime]) -> Decision:
|
|
176
|
+
if not all_outside_resets_by_name:
|
|
160
177
|
return decision
|
|
161
|
-
|
|
162
|
-
if decision.action
|
|
178
|
+
outside_name, outside_reset = min(all_outside_resets_by_name.items(), key=lambda each_item: each_item[1])
|
|
179
|
+
if decision.action != "wait":
|
|
180
|
+
return Decision("wait", None, outside_reset, f"account {outside_name} outside the roster is spent; next reset at {_time_text(outside_reset)}", "wait")
|
|
181
|
+
if decision.resets_at is not None and decision.resets_at <= outside_reset:
|
|
163
182
|
return decision
|
|
164
|
-
return Decision("wait", None,
|
|
183
|
+
return Decision("wait", None, outside_reset, f"no account has room; next reset at {_time_text(outside_reset)}", "wait")
|
|
165
184
|
|
|
166
185
|
|
|
167
186
|
def _write_report(path: Path, report: Report) -> None:
|
|
@@ -328,12 +347,17 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
328
347
|
|
|
329
348
|
def _parse_spent_mark(all_accounts_by_name: Mapping[str, Reading], mark: str, product: Product) -> tuple[Reading, datetime | None]:
|
|
330
349
|
name, separator, reset_text = mark.partition(":")
|
|
350
|
+
if not name:
|
|
351
|
+
raise BrokerConfigurationError(f"spent mark {mark!r} needs an account name")
|
|
331
352
|
try:
|
|
332
353
|
reset = datetime.fromtimestamp(float(reset_text), timezone.utc) if separator else None
|
|
333
354
|
except (ValueError, OSError, OverflowError) as error:
|
|
334
355
|
raise BrokerConfigurationError(f"invalid reset for account {name}") from error
|
|
335
356
|
reading = all_accounts_by_name.get(name)
|
|
336
|
-
|
|
357
|
+
if reading is None:
|
|
358
|
+
print(f"warning: spent mark names {name}, an account outside the roster; choose waits until its reset", file=sys.stderr)
|
|
359
|
+
return Reading(Account(product, name, Path(), False), None), reset
|
|
360
|
+
return reading, reset
|
|
337
361
|
|
|
338
362
|
|
|
339
363
|
def _save_spent_arguments(all_marks: Sequence[str], all_readings: Sequence[Reading], all_state: dict[str, object], now: datetime, product: Product) -> None:
|
|
@@ -696,6 +696,81 @@ def test_should_run_job_through_override(
|
|
|
696
696
|
assert calls == [b"input"]
|
|
697
697
|
|
|
698
698
|
|
|
699
|
+
@pytest.mark.parametrize("marked_name", ["retired", "first"])
|
|
700
|
+
def test_should_run_past_a_stored_spent_mark_beyond_the_platform_range(
|
|
701
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], marked_name: str
|
|
702
|
+
) -> None:
|
|
703
|
+
account = _account("first")
|
|
704
|
+
second = _account("second")
|
|
705
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter((account, second), {"first": _meters(80, 80), "second": _meters(80, 80)}))
|
|
706
|
+
_freeze_clock(monkeypatch, NOW)
|
|
707
|
+
marked_account = account if marked_name == "first" else Account(Product.CODEX, marked_name, Path(), False)
|
|
708
|
+
account_broker._save_state(
|
|
709
|
+
account_broker.broker_state_path(),
|
|
710
|
+
{"meters": {}, "spent": {account_broker._state_key(marked_account): 1e20}, "affinity": {}},
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
code = account_broker.main(("choose", "--product", "codex"))
|
|
714
|
+
|
|
715
|
+
decision = json.loads(capsys.readouterr().out)["decision"]
|
|
716
|
+
assert code == 0
|
|
717
|
+
assert decision["action"] == "run"
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def test_should_name_the_outside_spent_mark_when_it_forces_a_wait(
|
|
721
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
722
|
+
) -> None:
|
|
723
|
+
account = _account("first")
|
|
724
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter((account,), {"first": _meters(80, 80)}))
|
|
725
|
+
_freeze_clock(monkeypatch, NOW)
|
|
726
|
+
reset = NOW + timedelta(hours=2)
|
|
727
|
+
|
|
728
|
+
account_broker.main(("choose", "--product", "codex", "--spent", f"visitor:{int(reset.timestamp())}"))
|
|
729
|
+
|
|
730
|
+
decision = json.loads(capsys.readouterr().out)["decision"]
|
|
731
|
+
assert decision["action"] == "wait"
|
|
732
|
+
assert decision["reason"] == f"account visitor outside the roster is spent; next reset at {account_broker._time_text(reset)}"
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def test_should_warn_when_a_spent_mark_names_an_account_outside_the_roster(
|
|
736
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
737
|
+
) -> None:
|
|
738
|
+
account = _account("first")
|
|
739
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter((account,), {"first": _meters(80, 80)}))
|
|
740
|
+
_freeze_clock(monkeypatch, NOW)
|
|
741
|
+
|
|
742
|
+
account_broker.main(("choose", "--product", "codex", "--spent", "frist", "--spent", "first"))
|
|
743
|
+
|
|
744
|
+
all_warning_lines = capsys.readouterr().err.splitlines()
|
|
745
|
+
assert len(all_warning_lines) == 1
|
|
746
|
+
assert "frist" in all_warning_lines[0]
|
|
747
|
+
assert "outside the roster" in all_warning_lines[0]
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
@pytest.mark.parametrize("mark", ["", ":1800000000"])
|
|
751
|
+
def test_should_reject_a_spent_mark_without_an_account_name(
|
|
752
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], mark: str
|
|
753
|
+
) -> None:
|
|
754
|
+
account = _account("first")
|
|
755
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CODEX, _adapter((account,), {"first": _meters(80, 80)}))
|
|
756
|
+
|
|
757
|
+
code = account_broker.main(("choose", "--product", "codex", "--spent", mark))
|
|
758
|
+
|
|
759
|
+
assert code == 2
|
|
760
|
+
assert "account name" in capsys.readouterr().err
|
|
761
|
+
assert account_broker._load_state(account_broker.broker_state_path())["spent"] == {}
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def test_should_serialize_a_run_and_a_wait_decision() -> None:
|
|
765
|
+
account = _account("first")
|
|
766
|
+
reset = NOW + timedelta(hours=2)
|
|
767
|
+
|
|
768
|
+
run_payload = account_broker.decision_payload(account_broker.Decision("run", account, None, "first has 80% left", "normal"))
|
|
769
|
+
wait_payload = account_broker.decision_payload(account_broker.Decision("wait", None, reset, "no account has room", "wait"))
|
|
770
|
+
|
|
771
|
+
assert run_payload == {"action": "run", "account": "first", "home": str(Path("/profiles/first")), "reason": "first has 80% left", "tier": "normal", "resets_at": None}
|
|
772
|
+
assert wait_payload == {"action": "wait", "account": None, "home": None, "reason": "no account has room", "tier": "wait", "resets_at": reset.isoformat()}
|
|
773
|
+
|
|
699
774
|
|
|
700
775
|
def test_should_print_the_whole_rate_limit_result_for_one_home(
|
|
701
776
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
|