claude-dev-env 8.15.1 → 8.16.1

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.
@@ -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