claude-dev-env 8.46.25 → 8.46.27
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
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import errno
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import tempfile
|
|
@@ -428,13 +429,21 @@ def _resume_id(all_argv: Sequence[str]) -> str | None:
|
|
|
428
429
|
return None
|
|
429
430
|
|
|
430
431
|
|
|
432
|
+
def _resolve_command(all_argv: Sequence[str]) -> list[str]:
|
|
433
|
+
command_name, *all_arguments = all_argv
|
|
434
|
+
resolved_command = shutil.which(command_name)
|
|
435
|
+
if resolved_command is None and not os.path.dirname(command_name):
|
|
436
|
+
raise FileNotFoundError(errno.ENOENT, "command not found on PATH", command_name)
|
|
437
|
+
return [resolved_command or command_name, *all_arguments]
|
|
438
|
+
|
|
439
|
+
|
|
431
440
|
def _run_captured_subprocess(all_argv: Sequence[str], **options: object) -> subprocess.CompletedProcess[str]:
|
|
432
441
|
encoding = str(options.get("encoding") or "utf-8")
|
|
433
442
|
errors = str(options.get("errors") or "replace")
|
|
434
443
|
stdin_bytes = options.get("input")
|
|
435
444
|
with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
|
|
436
445
|
completion = subprocess.run(
|
|
437
|
-
|
|
446
|
+
_resolve_command(all_argv),
|
|
438
447
|
input=stdin_bytes,
|
|
439
448
|
stdout=stdout_file,
|
|
440
449
|
stderr=stderr_file,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
"""Floors
|
|
1
|
+
"""Floors, outcome shape, and time helpers for the account broker."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import dataclasses
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
10
|
+
from dev_env_scripts_constants import account_broker_constants as broker_constants
|
|
9
11
|
from dev_env_scripts_constants.account_broker_constants import (
|
|
10
12
|
ALL_CLAUDE_FLOORS,
|
|
11
13
|
ALL_CODEX_FLOORS,
|
|
@@ -47,3 +49,29 @@ def test_should_freeze_job_outcome() -> None:
|
|
|
47
49
|
outcome = JobOutcome(0, "", "", None, (), "served", None, None)
|
|
48
50
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
49
51
|
outcome.status = "wait"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_codex_usage_limit_signatures_loads_classifier_markers() -> None:
|
|
55
|
+
signatures = broker_constants.codex_usage_limit_signatures()
|
|
56
|
+
|
|
57
|
+
assert "rate limit" in signatures
|
|
58
|
+
assert "http 429" in signatures
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_utc_time_text_converts_an_offset_timestamp() -> None:
|
|
62
|
+
moment = datetime(2026, 10, 3, 7, 30, tzinfo=timezone(timedelta(hours=-4)))
|
|
63
|
+
|
|
64
|
+
assert broker_constants.utc_time_text(moment) == "2026-10-03T11:30:00+00:00"
|
|
65
|
+
assert broker_constants.utc_time_text(None) is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("timestamp", (None, 123, "not a timestamp", "2026-10-03T11:30:00"))
|
|
69
|
+
def test_parse_utc_time_rejects_values_without_a_timezone(timestamp: object) -> None:
|
|
70
|
+
assert broker_constants.parse_utc_time(timestamp) is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_parse_utc_time_converts_an_offset_timestamp() -> None:
|
|
74
|
+
parsed = broker_constants.parse_utc_time("2026-10-03T07:30:00-04:00")
|
|
75
|
+
|
|
76
|
+
assert parsed is not None
|
|
77
|
+
assert parsed.isoformat() == "2026-10-03T11:30:00+00:00"
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import io
|
|
5
|
+
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
from datetime import datetime, timedelta, timezone
|
|
7
8
|
from pathlib import Path
|
|
@@ -696,6 +697,20 @@ def test_should_run_job_through_override(
|
|
|
696
697
|
assert calls == [b"input"]
|
|
697
698
|
|
|
698
699
|
|
|
700
|
+
def test_should_record_start_failure_without_launching_a_command_missing_from_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
701
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CLAUDE, _adapter(
|
|
702
|
+
(_account("extra", Product.CLAUDE),), {"extra": _meters(80, 80)}
|
|
703
|
+
))
|
|
704
|
+
monkeypatch.setattr(shutil, "which", lambda name: None)
|
|
705
|
+
monkeypatch.setattr(account_broker.support.subprocess, "run", lambda *arguments, **options: pytest.fail("a missing command ran"))
|
|
706
|
+
|
|
707
|
+
outcome, _ = account_broker._execute(Product.CLAUDE, ("claude", "-p"), now=NOW)
|
|
708
|
+
|
|
709
|
+
assert outcome.attempts == (("extra", "start_failed"),)
|
|
710
|
+
assert outcome.status == "exhausted"
|
|
711
|
+
assert outcome.returncode == WAIT_EXIT_CODE
|
|
712
|
+
|
|
713
|
+
|
|
699
714
|
@pytest.mark.parametrize("marked_name", ["retired", "first"])
|
|
700
715
|
def test_should_run_past_a_stored_spent_mark_beyond_the_platform_range(
|
|
701
716
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], marked_name: str
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import errno
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
from datetime import datetime, timezone
|
|
8
9
|
from pathlib import Path
|
|
@@ -161,3 +162,31 @@ def test_should_raise_windows_lock_error_other_than_contention(tmp_path: Path) -
|
|
|
161
162
|
os.close(lock_descriptor)
|
|
162
163
|
|
|
163
164
|
assert all_calls == [lock_descriptor]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_should_launch_the_path_resolved_command_file(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
168
|
+
resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
|
|
169
|
+
monkeypatch.setattr(shutil, "which", lambda name: resolved_command if name == "claude" else None)
|
|
170
|
+
all_launched_argv: list[list[str]] = []
|
|
171
|
+
|
|
172
|
+
def fake_run(all_argv: list[str], **options: object) -> subprocess.CompletedProcess[bytes]:
|
|
173
|
+
all_launched_argv.append(list(all_argv))
|
|
174
|
+
return subprocess.CompletedProcess(all_argv, 0)
|
|
175
|
+
|
|
176
|
+
monkeypatch.setattr(support.subprocess, "run", fake_run)
|
|
177
|
+
|
|
178
|
+
completion = support.subprocess_runner(["claude", "-p"], input=b"", encoding="utf-8", errors="replace")
|
|
179
|
+
|
|
180
|
+
assert all_launched_argv == [[resolved_command, "-p"]]
|
|
181
|
+
assert completion.returncode == 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_should_refuse_to_launch_a_bare_command_missing_from_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
185
|
+
monkeypatch.setattr(shutil, "which", lambda name: None)
|
|
186
|
+
monkeypatch.setattr(support.subprocess, "run", lambda *arguments, **options: pytest.fail("a missing command ran"))
|
|
187
|
+
|
|
188
|
+
with pytest.raises(FileNotFoundError) as raised:
|
|
189
|
+
support.subprocess_runner(["claude", "-p"])
|
|
190
|
+
|
|
191
|
+
assert raised.value.errno == errno.ENOENT
|
|
192
|
+
assert raised.value.filename == "claude"
|