claude-dev-env 8.15.1 → 8.16.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/second-account-workers/SKILL.md +26 -8
- package/package.json +1 -1
- package/scripts/claude_account_choice.py +183 -52
- package/scripts/claude_account_profile.py +81 -49
- package/scripts/claude_account_worker.py +48 -15
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +36 -1
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +2 -0
- package/scripts/test_claude_account_choice.py +127 -17
- package/scripts/test_claude_account_profile.py +63 -16
- package/scripts/test_claude_account_worker.py +118 -0
|
@@ -3,12 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import subprocess
|
|
5
5
|
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
|
|
9
10
|
import pytest
|
|
10
11
|
|
|
12
|
+
import claude_account_choice
|
|
11
13
|
import claude_account_worker
|
|
14
|
+
from claude_chain_usage import AccountUsageMeters
|
|
12
15
|
from claude_account_worker import (
|
|
13
16
|
AccountSelection,
|
|
14
17
|
WorkerDependencies,
|
|
@@ -26,6 +29,8 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
|
26
29
|
WAIT_EXIT_CODE,
|
|
27
30
|
)
|
|
28
31
|
|
|
32
|
+
NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
|
|
33
|
+
|
|
29
34
|
|
|
30
35
|
class FakeProcess:
|
|
31
36
|
def __init__(
|
|
@@ -319,6 +324,119 @@ def test_should_pick_second_account_when_neither_meter_is_readable(
|
|
|
319
324
|
assert selection.config_dir == second_home
|
|
320
325
|
|
|
321
326
|
|
|
327
|
+
def test_should_read_profile_list_and_pick_later_extra(
|
|
328
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
329
|
+
) -> None:
|
|
330
|
+
main_home = tmp_path / ".claude"
|
|
331
|
+
main_home.mkdir()
|
|
332
|
+
(main_home / "extra-profiles.json").write_text(
|
|
333
|
+
json.dumps(["profile_a", "profile_b"]), encoding="utf-8"
|
|
334
|
+
)
|
|
335
|
+
profiles_root = tmp_path / "profiles"
|
|
336
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
337
|
+
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(profiles_root))
|
|
338
|
+
readings = {
|
|
339
|
+
profiles_root / "profile_a": AccountUsageMeters(
|
|
340
|
+
session_utilization=90.0,
|
|
341
|
+
session_resets_at=NOW + timedelta(hours=2),
|
|
342
|
+
weekly_utilization=95.0,
|
|
343
|
+
weekly_resets_at=NOW + timedelta(days=2),
|
|
344
|
+
),
|
|
345
|
+
profiles_root / "profile_b": AccountUsageMeters(
|
|
346
|
+
session_utilization=10.0,
|
|
347
|
+
session_resets_at=NOW + timedelta(hours=2),
|
|
348
|
+
weekly_utilization=20.0,
|
|
349
|
+
weekly_resets_at=NOW + timedelta(days=2),
|
|
350
|
+
),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
def read_meters(path: Path) -> AccountUsageMeters | None:
|
|
354
|
+
return readings.get(path.parent)
|
|
355
|
+
|
|
356
|
+
monkeypatch.setattr(claude_account_choice, "read_account_meters", read_meters)
|
|
357
|
+
monkeypatch.setattr(claude_account_worker, "read_account_meters", read_meters)
|
|
358
|
+
|
|
359
|
+
selection = select_account()
|
|
360
|
+
|
|
361
|
+
assert selection.account == "extra_2"
|
|
362
|
+
assert selection.config_dir == profiles_root / "profile_b"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_should_exit_three_when_every_listed_profile_is_full(
|
|
366
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
367
|
+
) -> None:
|
|
368
|
+
main_home = tmp_path / ".claude"
|
|
369
|
+
main_home.mkdir()
|
|
370
|
+
(main_home / "extra-profiles.json").write_text(
|
|
371
|
+
json.dumps(["profile_a", "profile_b"]), encoding="utf-8"
|
|
372
|
+
)
|
|
373
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
374
|
+
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(tmp_path / "profiles"))
|
|
375
|
+
blocked = AccountUsageMeters(
|
|
376
|
+
session_utilization=90.0,
|
|
377
|
+
session_resets_at=NOW + timedelta(hours=2),
|
|
378
|
+
weekly_utilization=95.0,
|
|
379
|
+
weekly_resets_at=NOW + timedelta(days=2),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def read_meters(path: Path) -> AccountUsageMeters | None:
|
|
383
|
+
return None if path.parent == main_home else blocked
|
|
384
|
+
|
|
385
|
+
monkeypatch.setattr(claude_account_choice, "read_account_meters", read_meters)
|
|
386
|
+
monkeypatch.setattr(claude_account_worker, "read_account_meters", read_meters)
|
|
387
|
+
report_file = tmp_path / "report.json"
|
|
388
|
+
|
|
389
|
+
exit_code = run_worker(
|
|
390
|
+
prompt_file=tmp_path / "unused.md",
|
|
391
|
+
cwd=tmp_path,
|
|
392
|
+
report_file=report_file,
|
|
393
|
+
model=None,
|
|
394
|
+
permission_mode="auto",
|
|
395
|
+
timeout_minutes=60,
|
|
396
|
+
decision_selector=select_account,
|
|
397
|
+
dependencies=WorkerDependencies(
|
|
398
|
+
process_factory=lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
399
|
+
AssertionError("wait must not start a process")
|
|
400
|
+
),
|
|
401
|
+
all_parent_environment_variables={},
|
|
402
|
+
binary_resolver=lambda _: None,
|
|
403
|
+
process_terminator=lambda _: None,
|
|
404
|
+
monotonic_clock=lambda: 1.0,
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
408
|
+
|
|
409
|
+
assert exit_code == WAIT_EXIT_CODE
|
|
410
|
+
assert report["account"] == CHOICE_WAIT
|
|
411
|
+
assert (NOW + timedelta(days=2)).isoformat() in report["reason"]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_should_set_later_extra_profile_environment(tmp_path: Path) -> None:
|
|
415
|
+
profile_dir = tmp_path / "profile_b"
|
|
416
|
+
exit_code, report, captured = _run_worker(
|
|
417
|
+
tmp_path,
|
|
418
|
+
selection=_selection("extra_2", config_dir=profile_dir),
|
|
419
|
+
process=FakeProcess(),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
assert exit_code == 0
|
|
423
|
+
assert report["account"] == "extra_2"
|
|
424
|
+
assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@pytest.mark.parametrize(
|
|
428
|
+
"contents",
|
|
429
|
+
['{', '{}', '[]', '["profile_a", "PROFILE_A"]', '["../outside"]', '["main"]'],
|
|
430
|
+
)
|
|
431
|
+
def test_should_reject_invalid_profile_lists(tmp_path: Path, contents: str) -> None:
|
|
432
|
+
main_home = tmp_path / ".claude"
|
|
433
|
+
main_home.mkdir()
|
|
434
|
+
(main_home / "extra-profiles.json").write_text(contents, encoding="utf-8")
|
|
435
|
+
|
|
436
|
+
with pytest.raises(ValueError):
|
|
437
|
+
claude_account_worker.extra_config_directories(main_home)
|
|
438
|
+
|
|
439
|
+
|
|
322
440
|
def test_should_require_prompt_file_and_report_file_flags() -> None:
|
|
323
441
|
parser = build_argument_parser()
|
|
324
442
|
|