claude-dev-env 8.43.5 → 8.44.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/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/cli-chain.md +9 -12
- package/_shared/advisor/reference/lifecycle.md +1 -1
- package/_shared/advisor/reference/third-party-bind.md +3 -3
- package/bin/install.test.mjs +1 -1
- package/docs/second-claude-account.md +11 -13
- package/hooks/blocking/reply_length_gate.py +43 -11
- package/hooks/blocking/test_reply_length_gate.py +55 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/reply_length_gate_constants.py +11 -0
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +29 -39
- package/scripts/claude_account_worker.py +46 -297
- package/scripts/claude_account_worker_process.py +18 -154
- package/scripts/claude_account_worker_report.py +8 -4
- package/scripts/claude_chain_usage.py +7 -271
- package/scripts/codec_forwarding_test_support.py +11 -65
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
- package/scripts/invoke_code_review.py +34 -56
- package/scripts/resolve_worker_spawn.py +40 -72
- package/scripts/test_account_broker_guard.py +0 -4
- package/scripts/test_claude_account_worker.py +105 -350
- package/scripts/test_claude_chain_usage.py +50 -510
- package/scripts/test_dispatcher_profile_import.py +5 -9
- package/scripts/test_invoke_code_review.py +7 -6
- package/scripts/test_invoke_code_review_chain.py +26 -0
- package/scripts/test_invoke_code_review_cli.py +8 -8
- package/scripts/test_invoke_code_review_codec.py +3 -11
- package/scripts/test_invoke_code_review_contract.py +5 -1
- package/scripts/test_resolve_worker_spawn.py +118 -265
- package/scripts/test_resolve_worker_spawn_codec.py +9 -21
- package/scripts/tests/test_code_review_constants.py +2 -2
- package/scripts/claude_account_choice.py +0 -422
- package/scripts/claude_chain_runner.py +0 -1289
- package/scripts/test_claude_account_choice.py +0 -355
- package/scripts/test_claude_chain_runner.py +0 -1839
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import io
|
|
6
5
|
import json
|
|
7
6
|
import subprocess
|
|
8
7
|
import sys
|
|
9
8
|
import threading
|
|
10
|
-
from collections.abc import Sequence
|
|
11
9
|
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
12
11
|
from pathlib import Path
|
|
13
|
-
from typing import Self
|
|
14
12
|
|
|
15
13
|
import pytest
|
|
16
14
|
|
|
@@ -18,17 +16,17 @@ _SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
|
18
16
|
if str(_SCRIPTS_DIR) not in sys.path:
|
|
19
17
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
20
18
|
|
|
21
|
-
import
|
|
19
|
+
import account_broker
|
|
22
20
|
import invoke_code_review as invoker
|
|
23
21
|
import resolve_worker_spawn as dispatcher # noqa: E402
|
|
24
|
-
from
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
from dev_env_scripts_constants.account_broker_constants import (
|
|
23
|
+
Account,
|
|
24
|
+
BrokerConfigurationError,
|
|
25
|
+
JobOutcome,
|
|
26
|
+
Meters,
|
|
27
|
+
Product,
|
|
28
|
+
ProductAdapter,
|
|
29
|
+
WAIT_EXIT_CODE,
|
|
32
30
|
)
|
|
33
31
|
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
34
32
|
AGENT_FLAG,
|
|
@@ -64,7 +62,6 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
64
62
|
RESULT_KEY_TIER_USED,
|
|
65
63
|
SINGLE_TURN_FLAG,
|
|
66
64
|
SPAWN_CONFIG_ERROR_EXIT_CODE,
|
|
67
|
-
SPAWN_EXHAUSTED_EXIT_CODE,
|
|
68
65
|
SPAWN_SERVED_EXIT_CODE,
|
|
69
66
|
TIER_CLAUDE_AGENT,
|
|
70
67
|
TIER_CLAUDE_HEADLESS,
|
|
@@ -91,7 +88,6 @@ FIXTURE_ROLE = "code-quality-agent"
|
|
|
91
88
|
MIN_WORKER_TIMEOUT_SECONDS_CONSTANT_NAME = "MIN_WORKER_TIMEOUT_SECONDS"
|
|
92
89
|
LARGE_PROMPT_CHARACTER_COUNT = 40000
|
|
93
90
|
WINDOWS_SAFE_ARGV_ELEMENT_CEILING = 8192
|
|
94
|
-
LOCK_WAIT_TIMEOUT_SECONDS = 1
|
|
95
91
|
EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE = Path(
|
|
96
92
|
ALL_AGENT_FILENAMES_BY_ROLE[DEFAULT_ROLE][0]
|
|
97
93
|
).stem
|
|
@@ -135,25 +131,29 @@ def _claude_served(
|
|
|
135
131
|
stdout: str = FIXTURE_CLAUDE_STDOUT,
|
|
136
132
|
*,
|
|
137
133
|
returncode: int = FIXTURE_CLAUDE_RETURNCODE,
|
|
138
|
-
) ->
|
|
139
|
-
return
|
|
140
|
-
served_command="claude",
|
|
134
|
+
) -> JobOutcome:
|
|
135
|
+
return JobOutcome(
|
|
141
136
|
returncode=returncode,
|
|
142
137
|
stdout=stdout,
|
|
143
138
|
stderr="",
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
account_name="main",
|
|
140
|
+
attempts=(("main", "served"),),
|
|
141
|
+
status="served" if returncode == 0 else "advisor_blocked",
|
|
142
|
+
session_id=None,
|
|
143
|
+
wait_reset_at=None,
|
|
146
144
|
)
|
|
147
145
|
|
|
148
146
|
|
|
149
|
-
def _claude_exhausted() ->
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
returncode=
|
|
147
|
+
def _claude_exhausted() -> JobOutcome:
|
|
148
|
+
return JobOutcome(
|
|
149
|
+
account_name=None,
|
|
150
|
+
returncode=WAIT_EXIT_CODE,
|
|
153
151
|
stdout="",
|
|
154
|
-
stderr="
|
|
155
|
-
attempts=(
|
|
156
|
-
|
|
152
|
+
stderr="",
|
|
153
|
+
attempts=(("main", "usage_limited"),),
|
|
154
|
+
status="exhausted",
|
|
155
|
+
session_id=None,
|
|
156
|
+
wait_reset_at=datetime(2026, 10, 4, tzinfo=timezone.utc),
|
|
157
157
|
)
|
|
158
158
|
|
|
159
159
|
|
|
@@ -174,8 +174,7 @@ class SeamCallLog:
|
|
|
174
174
|
claude_calls: int = 0
|
|
175
175
|
claude_arguments: list[str] | None = None
|
|
176
176
|
host_profile_calls: int = 0
|
|
177
|
-
|
|
178
|
-
claude_stdin_path: Path | None = None
|
|
177
|
+
claude_stdin_text: str | None = None
|
|
179
178
|
claude_working_directory: Path | None = None
|
|
180
179
|
grok_keyword_arguments: dict[str, object] | None = None
|
|
181
180
|
max_argv_element_length: int = 0
|
|
@@ -187,7 +186,7 @@ def _install_seams(
|
|
|
187
186
|
*,
|
|
188
187
|
preflight_outcome: PreflightOutcome = _usable_preflight(),
|
|
189
188
|
grok_outcome: GrokRunnerOutcome | None = None,
|
|
190
|
-
claude_outcome:
|
|
189
|
+
claude_outcome: JobOutcome | BaseException | None = None,
|
|
191
190
|
host_profile: str = HOST_PROFILE_CLAUDE,
|
|
192
191
|
) -> SeamCallLog:
|
|
193
192
|
call_log = SeamCallLog()
|
|
@@ -202,21 +201,18 @@ def _install_seams(
|
|
|
202
201
|
assert grok_outcome is not None
|
|
203
202
|
return grok_outcome
|
|
204
203
|
|
|
205
|
-
def fake_claude(
|
|
206
|
-
|
|
207
|
-
) -> ChainInvocationOutcome:
|
|
204
|
+
def fake_claude(product: Product, all_claude_arguments: list[str], **options: object) -> JobOutcome:
|
|
205
|
+
assert product is Product.CLAUDE
|
|
208
206
|
call_log.claude_calls += 1
|
|
209
|
-
call_log.claude_arguments = list(all_claude_arguments)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
check=False,
|
|
216
|
-
)
|
|
207
|
+
call_log.claude_arguments = list(all_claude_arguments[1:])
|
|
208
|
+
call_log.max_argv_element_length = max(map(len, all_claude_arguments))
|
|
209
|
+
call_log.claude_stdin_text = options.get("stdin_text")
|
|
210
|
+
if options.get("cwd") is not None:
|
|
211
|
+
call_log.claude_working_directory = Path(options["cwd"])
|
|
212
|
+
call_log.all_observed_working_directories.append(Path(options["cwd"]))
|
|
217
213
|
if isinstance(claude_outcome, BaseException):
|
|
218
214
|
raise claude_outcome
|
|
219
|
-
assert isinstance(claude_outcome,
|
|
215
|
+
assert isinstance(claude_outcome, JobOutcome)
|
|
220
216
|
return claude_outcome
|
|
221
217
|
|
|
222
218
|
def fake_host_profile(
|
|
@@ -231,37 +227,6 @@ def _install_seams(
|
|
|
231
227
|
monkeypatch.setattr(dispatcher, "spawn_claude_runner", fake_claude)
|
|
232
228
|
monkeypatch.setattr(dispatcher, "spawn_host_profile_detector", fake_host_profile)
|
|
233
229
|
|
|
234
|
-
def _tracking_subprocess_runner(
|
|
235
|
-
all_invocation_tokens: Sequence[str],
|
|
236
|
-
*all_positionals: object,
|
|
237
|
-
**all_keywords: object,
|
|
238
|
-
) -> subprocess.CompletedProcess[str]:
|
|
239
|
-
del all_positionals
|
|
240
|
-
for each_token in all_invocation_tokens:
|
|
241
|
-
call_log.max_argv_element_length = max(
|
|
242
|
-
call_log.max_argv_element_length, len(str(each_token))
|
|
243
|
-
)
|
|
244
|
-
maybe_stdin = all_keywords.get("stdin")
|
|
245
|
-
if maybe_stdin is not None and maybe_stdin is not subprocess.DEVNULL:
|
|
246
|
-
call_log.is_stdin_from_prompt_file = True
|
|
247
|
-
maybe_name = getattr(maybe_stdin, "name", None)
|
|
248
|
-
if maybe_name is not None:
|
|
249
|
-
call_log.claude_stdin_path = Path(str(maybe_name))
|
|
250
|
-
maybe_cwd = all_keywords.get("cwd")
|
|
251
|
-
if maybe_cwd is not None:
|
|
252
|
-
working_directory = Path(str(maybe_cwd))
|
|
253
|
-
call_log.claude_working_directory = working_directory
|
|
254
|
-
call_log.all_observed_working_directories.append(working_directory)
|
|
255
|
-
return subprocess.CompletedProcess(
|
|
256
|
-
args=list(all_invocation_tokens),
|
|
257
|
-
returncode=0,
|
|
258
|
-
stdout="{}",
|
|
259
|
-
stderr="",
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
monkeypatch.setattr(
|
|
263
|
-
chain_runner, "chain_subprocess_runner", _tracking_subprocess_runner
|
|
264
|
-
)
|
|
265
230
|
return call_log
|
|
266
231
|
|
|
267
232
|
|
|
@@ -487,8 +452,7 @@ def test_grok_auth_failed_on_third_party_runs_tier_three(
|
|
|
487
452
|
AGENT_FLAG,
|
|
488
453
|
FIXTURE_ROLE,
|
|
489
454
|
]
|
|
490
|
-
assert call_log.
|
|
491
|
-
assert call_log.claude_stdin_path == prompt_file
|
|
455
|
+
assert call_log.claude_stdin_text == FIXTURE_PROMPT_TEXT
|
|
492
456
|
assert call_log.claude_working_directory == working_directory
|
|
493
457
|
assert spawn_outcome.all_attempts[0].tier == TIER_GROK
|
|
494
458
|
assert spawn_outcome.all_attempts[0].reason == CLASSIFICATION_AUTH_FAILURE
|
|
@@ -496,11 +460,11 @@ def test_grok_auth_failed_on_third_party_runs_tier_three(
|
|
|
496
460
|
assert spawn_outcome.all_attempts[1].is_ok is True
|
|
497
461
|
|
|
498
462
|
|
|
499
|
-
def
|
|
463
|
+
def test_tier_three_wait_reports_reset_and_stops(
|
|
500
464
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
501
465
|
) -> None:
|
|
502
466
|
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
503
|
-
_install_seams(
|
|
467
|
+
call_log = _install_seams(
|
|
504
468
|
monkeypatch,
|
|
505
469
|
grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
|
|
506
470
|
claude_outcome=_claude_exhausted(),
|
|
@@ -522,11 +486,14 @@ def test_tier_three_exhausted_returns_exit_two(
|
|
|
522
486
|
]
|
|
523
487
|
)
|
|
524
488
|
|
|
525
|
-
assert exit_code ==
|
|
489
|
+
assert exit_code == WAIT_EXIT_CODE
|
|
526
490
|
captured = capsys.readouterr()
|
|
527
491
|
parsed_payload = json.loads(captured.out)
|
|
528
492
|
assert parsed_payload[RESULT_KEY_OK] is False
|
|
529
493
|
assert parsed_payload[RESULT_KEY_TIER_USED] is None
|
|
494
|
+
assert parsed_payload["status"] == "exhausted"
|
|
495
|
+
assert parsed_payload["wait_reset_at"] == datetime(2026, 10, 4, tzinfo=timezone.utc).isoformat()
|
|
496
|
+
assert call_log.claude_calls == 1
|
|
530
497
|
|
|
531
498
|
|
|
532
499
|
def test_config_error_returns_exit_three(
|
|
@@ -536,7 +503,7 @@ def test_config_error_returns_exit_three(
|
|
|
536
503
|
_install_seams(
|
|
537
504
|
monkeypatch,
|
|
538
505
|
grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
|
|
539
|
-
claude_outcome=
|
|
506
|
+
claude_outcome=BrokerConfigurationError("chain config missing"),
|
|
540
507
|
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
541
508
|
)
|
|
542
509
|
|
|
@@ -660,6 +627,8 @@ def test_cli_stdout_carries_only_json_result(
|
|
|
660
627
|
RESULT_KEY_ATTEMPTS,
|
|
661
628
|
RESULT_KEY_OUTPUT,
|
|
662
629
|
RESULT_KEY_RETURNCODE,
|
|
630
|
+
"status",
|
|
631
|
+
"wait_reset_at",
|
|
663
632
|
}
|
|
664
633
|
assert reparsed[RESULT_KEY_TIER_USED] == TIER_GROK
|
|
665
634
|
assert reparsed[RESULT_KEY_OK] is True
|
|
@@ -797,8 +766,7 @@ def test_enable_claude_tier_flag_reaches_tier_three_on_claude_host(
|
|
|
797
766
|
|
|
798
767
|
assert exit_code == SPAWN_SERVED_EXIT_CODE
|
|
799
768
|
assert call_log.claude_calls == 1
|
|
800
|
-
assert call_log.
|
|
801
|
-
assert call_log.claude_stdin_path == prompt_file
|
|
769
|
+
assert call_log.claude_stdin_text == FIXTURE_PROMPT_TEXT
|
|
802
770
|
assert call_log.claude_working_directory == working_directory
|
|
803
771
|
assert call_log.claude_arguments is not None
|
|
804
772
|
assert PROMPT_FILE_FLAG not in call_log.claude_arguments
|
|
@@ -892,8 +860,7 @@ def test_large_prompt_stays_out_of_claude_argv(
|
|
|
892
860
|
assert call_log.claude_arguments is not None
|
|
893
861
|
assert large_prompt_text not in call_log.claude_arguments
|
|
894
862
|
assert call_log.max_argv_element_length < WINDOWS_SAFE_ARGV_ELEMENT_CEILING
|
|
895
|
-
assert call_log.
|
|
896
|
-
assert call_log.claude_stdin_path == prompt_file
|
|
863
|
+
assert call_log.claude_stdin_text == large_prompt_text
|
|
897
864
|
|
|
898
865
|
|
|
899
866
|
def test_missing_prompt_file_returns_json_config_exit(
|
|
@@ -1027,19 +994,9 @@ def test_prompt_open_failure_not_classified_as_executable_not_found(
|
|
|
1027
994
|
lambda *all_positionals, **all_keywords: HOST_PROFILE_THIRD_PARTY,
|
|
1028
995
|
)
|
|
1029
996
|
monkeypatch.setattr(
|
|
1030
|
-
|
|
1031
|
-
"
|
|
1032
|
-
lambda
|
|
1033
|
-
)
|
|
1034
|
-
monkeypatch.setattr(
|
|
1035
|
-
chain_runner,
|
|
1036
|
-
"chain_subprocess_runner",
|
|
1037
|
-
lambda *all_positionals, **all_keywords: subprocess.CompletedProcess(
|
|
1038
|
-
args=["claude"],
|
|
1039
|
-
returncode=0,
|
|
1040
|
-
stdout="{}",
|
|
1041
|
-
stderr="",
|
|
1042
|
-
),
|
|
997
|
+
dispatcher,
|
|
998
|
+
"spawn_claude_runner",
|
|
999
|
+
lambda *args, **kwargs: pytest.fail("broker must not run"),
|
|
1043
1000
|
)
|
|
1044
1001
|
_deny_prompt_open(
|
|
1045
1002
|
monkeypatch,
|
|
@@ -1073,27 +1030,22 @@ def test_prompt_open_failure_not_classified_as_executable_not_found(
|
|
|
1073
1030
|
assert "executable_not_found" not in all_attempt_reasons
|
|
1074
1031
|
|
|
1075
1032
|
|
|
1076
|
-
def
|
|
1033
|
+
def test_headless_jobs_keep_distinct_cwds(
|
|
1077
1034
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1078
1035
|
) -> None:
|
|
1079
|
-
prompt_file = tmp_path
|
|
1080
|
-
prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding="utf-8")
|
|
1081
|
-
first_working_directory = tmp_path / "project-a"
|
|
1036
|
+
prompt_file, first_working_directory, run_state_directory = _paths(tmp_path)
|
|
1082
1037
|
second_working_directory = tmp_path / "project-b"
|
|
1083
|
-
first_working_directory.mkdir()
|
|
1084
1038
|
second_working_directory.mkdir()
|
|
1085
|
-
run_state_directory = tmp_path / "run-state"
|
|
1086
|
-
run_state_directory.mkdir()
|
|
1087
1039
|
call_log = _install_seams(
|
|
1088
1040
|
monkeypatch,
|
|
1089
1041
|
grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
|
|
1090
1042
|
claude_outcome=_claude_served(),
|
|
1091
1043
|
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
1092
1044
|
)
|
|
1093
|
-
|
|
1045
|
+
errors: list[Exception] = []
|
|
1094
1046
|
barrier = threading.Barrier(2)
|
|
1095
1047
|
|
|
1096
|
-
def
|
|
1048
|
+
def run_with_directory(working_directory: Path) -> None:
|
|
1097
1049
|
try:
|
|
1098
1050
|
barrier.wait(timeout=5)
|
|
1099
1051
|
dispatcher.resolve_worker_spawn(
|
|
@@ -1103,202 +1055,103 @@ def test_headless_chain_runner_lock_serializes_distinct_cwds(
|
|
|
1103
1055
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
1104
1056
|
is_claude_tier_enabled=False,
|
|
1105
1057
|
run_state_directory=run_state_directory,
|
|
1106
|
-
|
|
1107
|
-
except (OSError, RuntimeError, ValueError, AssertionError) as
|
|
1108
|
-
|
|
1058
|
+
)
|
|
1059
|
+
except (OSError, RuntimeError, ValueError, AssertionError) as error:
|
|
1060
|
+
errors.append(error)
|
|
1109
1061
|
|
|
1110
|
-
first_thread = threading.Thread(
|
|
1111
|
-
|
|
1112
|
-
)
|
|
1113
|
-
second_thread = threading.Thread(
|
|
1114
|
-
target=_run_with_working_directory, args=(second_working_directory,)
|
|
1115
|
-
)
|
|
1062
|
+
first_thread = threading.Thread(target=run_with_directory, args=(first_working_directory,))
|
|
1063
|
+
second_thread = threading.Thread(target=run_with_directory, args=(second_working_directory,))
|
|
1116
1064
|
first_thread.start()
|
|
1117
1065
|
second_thread.start()
|
|
1118
1066
|
first_thread.join(timeout=10)
|
|
1119
1067
|
second_thread.join(timeout=10)
|
|
1120
1068
|
|
|
1121
|
-
assert
|
|
1069
|
+
assert errors == []
|
|
1122
1070
|
assert call_log.claude_calls == 2
|
|
1123
1071
|
assert set(call_log.all_observed_working_directories) == {
|
|
1124
|
-
first_working_directory,
|
|
1125
|
-
second_working_directory,
|
|
1072
|
+
first_working_directory, second_working_directory
|
|
1126
1073
|
}
|
|
1127
|
-
assert chain_runner.chain_subprocess_runner is not None
|
|
1128
1074
|
|
|
1129
1075
|
|
|
1130
|
-
def
|
|
1076
|
+
def test_code_review_and_dispatcher_forward_their_own_cwds(
|
|
1131
1077
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1132
1078
|
) -> None:
|
|
1133
|
-
first_working_directory = tmp_path / "
|
|
1134
|
-
second_working_directory = tmp_path / "
|
|
1079
|
+
first_working_directory = tmp_path / "review"
|
|
1080
|
+
second_working_directory = tmp_path / "spawn"
|
|
1135
1081
|
first_working_directory.mkdir()
|
|
1136
1082
|
second_working_directory.mkdir()
|
|
1137
|
-
|
|
1138
|
-
all_errors: list[BaseException] = []
|
|
1139
|
-
|
|
1140
|
-
class CoordinatedLock:
|
|
1141
|
-
def __init__(self) -> None:
|
|
1142
|
-
self._lock = threading.Lock()
|
|
1143
|
-
self.first_acquired = threading.Event()
|
|
1144
|
-
self.second_attempted = threading.Event()
|
|
1145
|
-
self.enter_count = 0
|
|
1146
|
-
|
|
1147
|
-
def __enter__(self) -> Self:
|
|
1148
|
-
self.enter_count += 1
|
|
1149
|
-
if self.enter_count == 1:
|
|
1150
|
-
self.first_acquired.set()
|
|
1151
|
-
elif self.enter_count == 2:
|
|
1152
|
-
self.second_attempted.set()
|
|
1153
|
-
self._lock.acquire()
|
|
1154
|
-
return self
|
|
1155
|
-
|
|
1156
|
-
def __exit__(self, *all_positionals: object) -> None:
|
|
1157
|
-
del all_positionals
|
|
1158
|
-
self._lock.release()
|
|
1159
|
-
|
|
1160
|
-
coordinated_lock = CoordinatedLock()
|
|
1161
|
-
|
|
1162
|
-
def tracking_subprocess_runner(
|
|
1163
|
-
all_invocation_tokens: Sequence[str],
|
|
1164
|
-
*all_positionals: object,
|
|
1165
|
-
**all_keywords: object,
|
|
1166
|
-
) -> subprocess.CompletedProcess[str]:
|
|
1167
|
-
del all_invocation_tokens, all_positionals
|
|
1168
|
-
observed_working_directories.append(Path(str(all_keywords["cwd"])))
|
|
1169
|
-
return subprocess.CompletedProcess(
|
|
1170
|
-
args=["claude"], returncode=0, stdout="{}", stderr=""
|
|
1171
|
-
)
|
|
1083
|
+
observed: list[Path] = []
|
|
1172
1084
|
|
|
1173
|
-
def
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
del all_claude_arguments, timeout_seconds
|
|
1177
|
-
if not coordinated_lock.second_attempted.wait(
|
|
1178
|
-
timeout=LOCK_WAIT_TIMEOUT_SECONDS
|
|
1179
|
-
):
|
|
1180
|
-
all_errors.append(AssertionError("shared lock was never contended"))
|
|
1181
|
-
return _claude_served()
|
|
1182
|
-
chain_runner.chain_subprocess_runner(
|
|
1183
|
-
["claude"], capture_output=True, text=True, timeout=1, check=False
|
|
1184
|
-
)
|
|
1085
|
+
def runner(product: Product, argv: list[str], **options: object) -> JobOutcome:
|
|
1086
|
+
assert product is Product.CLAUDE
|
|
1087
|
+
observed.append(Path(options["cwd"]))
|
|
1185
1088
|
return _claude_served()
|
|
1186
1089
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
) -> ChainInvocationOutcome:
|
|
1190
|
-
del all_claude_arguments, timeout_seconds
|
|
1191
|
-
chain_runner.chain_subprocess_runner(
|
|
1192
|
-
["claude"], capture_output=True, text=True, timeout=1, check=False
|
|
1193
|
-
)
|
|
1194
|
-
return _claude_served()
|
|
1090
|
+
monkeypatch.setattr(invoker, "review_claude_runner", runner)
|
|
1091
|
+
monkeypatch.setattr(dispatcher, "spawn_claude_runner", runner)
|
|
1195
1092
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1093
|
+
invoker._run_claude_with_empty_stdin(
|
|
1094
|
+
["-p", "review"], timeout_seconds=1, working_directory=first_working_directory
|
|
1198
1095
|
)
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1096
|
+
dispatcher._run_claude_with_headless_overrides(
|
|
1097
|
+
["-p", "spawn"],
|
|
1098
|
+
timeout_seconds=1,
|
|
1099
|
+
working_directory=second_working_directory,
|
|
1100
|
+
prompt_text=FIXTURE_PROMPT_TEXT,
|
|
1202
1101
|
)
|
|
1203
|
-
monkeypatch.setattr(invoker, "review_claude_runner", run_review_runner)
|
|
1204
|
-
monkeypatch.setattr(dispatcher, "spawn_claude_runner", run_spawn_runner)
|
|
1205
1102
|
|
|
1206
|
-
|
|
1207
|
-
try:
|
|
1208
|
-
invoker._run_claude_with_empty_stdin(
|
|
1209
|
-
["-p", "review"],
|
|
1210
|
-
timeout_seconds=1,
|
|
1211
|
-
working_directory=first_working_directory,
|
|
1212
|
-
)
|
|
1213
|
-
except Exception as raised_error: # noqa: BLE001
|
|
1214
|
-
all_errors.append(raised_error)
|
|
1103
|
+
assert observed == [first_working_directory, second_working_directory]
|
|
1215
1104
|
|
|
1216
|
-
def run_spawn() -> None:
|
|
1217
|
-
try:
|
|
1218
|
-
dispatcher._run_claude_with_headless_overrides(
|
|
1219
|
-
["-p", "spawn"],
|
|
1220
|
-
timeout_seconds=1,
|
|
1221
|
-
working_directory=second_working_directory,
|
|
1222
|
-
prompt_stdin=io.StringIO(FIXTURE_PROMPT_TEXT),
|
|
1223
|
-
)
|
|
1224
|
-
except Exception as raised_error: # noqa: BLE001
|
|
1225
|
-
all_errors.append(raised_error)
|
|
1226
|
-
|
|
1227
|
-
review_thread = threading.Thread(target=run_review)
|
|
1228
|
-
spawn_thread = threading.Thread(target=run_spawn)
|
|
1229
|
-
review_thread.start()
|
|
1230
|
-
coordinated_lock.first_acquired.wait(timeout=LOCK_WAIT_TIMEOUT_SECONDS)
|
|
1231
|
-
spawn_thread.start()
|
|
1232
|
-
review_thread.join(timeout=10)
|
|
1233
|
-
spawn_thread.join(timeout=10)
|
|
1234
|
-
|
|
1235
|
-
assert all_errors == []
|
|
1236
|
-
assert coordinated_lock.enter_count == 2
|
|
1237
|
-
assert observed_working_directories == [
|
|
1238
|
-
first_working_directory,
|
|
1239
|
-
second_working_directory,
|
|
1240
|
-
]
|
|
1241
|
-
assert chain_runner.chain_subprocess_runner is tracking_subprocess_runner
|
|
1242
1105
|
|
|
1243
|
-
|
|
1244
|
-
def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
|
|
1106
|
+
def test_usage_limit_fallover_delivers_full_prompt_to_each_account(
|
|
1245
1107
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1246
1108
|
) -> None:
|
|
1247
1109
|
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
"
|
|
1251
|
-
lambda **_keyword_arguments: _fallthrough_preflight(REASON_GROK_AUTH_FAILED),
|
|
1252
|
-
)
|
|
1253
|
-
monkeypatch.setattr(
|
|
1254
|
-
dispatcher,
|
|
1255
|
-
"spawn_host_profile_detector",
|
|
1256
|
-
lambda *all_positionals, **all_keywords: HOST_PROFILE_THIRD_PARTY,
|
|
1110
|
+
accounts = (
|
|
1111
|
+
Account(Product.CLAUDE, "first", tmp_path / "first"),
|
|
1112
|
+
Account(Product.CLAUDE, "second", tmp_path / "second"),
|
|
1257
1113
|
)
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
lambda
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1114
|
+
reset_at = datetime(2026, 10, 5, tzinfo=timezone.utc)
|
|
1115
|
+
adapter = ProductAdapter(
|
|
1116
|
+
lambda: accounts,
|
|
1117
|
+
lambda account: Meters(80, reset_at, 80, reset_at),
|
|
1118
|
+
"CLAUDE_CONFIG_DIR",
|
|
1119
|
+
("usage limit",),
|
|
1120
|
+
False,
|
|
1265
1121
|
)
|
|
1266
|
-
|
|
1122
|
+
observed: list[tuple[str, bytes]] = []
|
|
1267
1123
|
|
|
1268
|
-
def
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
command_name = str(all_invocation_tokens[0])
|
|
1275
|
-
read_prompt = getattr(all_keywords.get("stdin"), "read", None)
|
|
1276
|
-
prompt_text_by_command[command_name] = read_prompt() if read_prompt else ""
|
|
1277
|
-
is_primary = command_name == "claude"
|
|
1278
|
-
return subprocess.CompletedProcess(
|
|
1279
|
-
args=list(all_invocation_tokens),
|
|
1280
|
-
returncode=(
|
|
1281
|
-
FIXTURE_FAILED_RETURNCODE if is_primary else SPAWN_SERVED_EXIT_CODE
|
|
1282
|
-
),
|
|
1283
|
-
stdout="usage limit reached" if is_primary else FIXTURE_CLAUDE_STDOUT,
|
|
1284
|
-
stderr="",
|
|
1285
|
-
)
|
|
1124
|
+
def runner(argv: list[str], **options: object) -> subprocess.CompletedProcess[str]:
|
|
1125
|
+
account_name = Path(options["env"]["CLAUDE_CONFIG_DIR"]).name
|
|
1126
|
+
observed.append((account_name, options["input"]))
|
|
1127
|
+
if account_name == "first":
|
|
1128
|
+
return subprocess.CompletedProcess(argv, 1, "usage limit reached", "")
|
|
1129
|
+
return subprocess.CompletedProcess(argv, 0, FIXTURE_CLAUDE_STDOUT, "")
|
|
1286
1130
|
|
|
1131
|
+
monkeypatch.setitem(account_broker.all_product_adapters, Product.CLAUDE, adapter)
|
|
1132
|
+
monkeypatch.setattr(account_broker, "broker_state_path", lambda: tmp_path / "state.json")
|
|
1287
1133
|
monkeypatch.setattr(
|
|
1288
|
-
|
|
1134
|
+
dispatcher,
|
|
1135
|
+
"spawn_preflight_runner",
|
|
1136
|
+
lambda **kwargs: _fallthrough_preflight(REASON_GROK_AUTH_FAILED),
|
|
1289
1137
|
)
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
role=DEFAULT_ROLE,
|
|
1293
|
-
prompt_file=prompt_file,
|
|
1294
|
-
working_directory=working_directory,
|
|
1295
|
-
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
1296
|
-
is_claude_tier_enabled=False,
|
|
1297
|
-
run_state_directory=run_state_directory,
|
|
1138
|
+
monkeypatch.setattr(
|
|
1139
|
+
dispatcher, "spawn_host_profile_detector", lambda: HOST_PROFILE_THIRD_PARTY
|
|
1298
1140
|
)
|
|
1141
|
+
with account_broker.override_subprocess_runner(runner):
|
|
1142
|
+
outcome = dispatcher.resolve_worker_spawn(
|
|
1143
|
+
role=DEFAULT_ROLE,
|
|
1144
|
+
prompt_file=prompt_file,
|
|
1145
|
+
working_directory=working_directory,
|
|
1146
|
+
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
1147
|
+
is_claude_tier_enabled=False,
|
|
1148
|
+
run_state_directory=run_state_directory,
|
|
1149
|
+
)
|
|
1299
1150
|
|
|
1300
|
-
assert
|
|
1301
|
-
|
|
1151
|
+
assert observed == [
|
|
1152
|
+
("first", FIXTURE_PROMPT_TEXT.encode("utf-8")),
|
|
1153
|
+
("second", FIXTURE_PROMPT_TEXT.encode("utf-8")),
|
|
1154
|
+
]
|
|
1302
1155
|
assert outcome.tier_used == TIER_CLAUDE_HEADLESS
|
|
1303
1156
|
assert outcome.is_ok is True
|
|
1304
1157
|
|
|
@@ -24,12 +24,9 @@ FIXTURE_PROMPT_TEXT = "do the work"
|
|
|
24
24
|
|
|
25
25
|
def _install_dispatcher_codec_seams(
|
|
26
26
|
monkeypatch: pytest.MonkeyPatch,
|
|
27
|
-
*,
|
|
28
|
-
all_chain_codec_keywords: dict[str, str],
|
|
29
27
|
) -> dict[str, object]:
|
|
30
28
|
return install_codec_seams(
|
|
31
29
|
monkeypatch,
|
|
32
|
-
all_chain_codec_keywords=all_chain_codec_keywords,
|
|
33
30
|
chain_stdout=FIXTURE_CHAIN_STDOUT,
|
|
34
31
|
runner_host=dispatcher,
|
|
35
32
|
runner_attribute_name="spawn_claude_runner",
|
|
@@ -42,16 +39,12 @@ def _run_headless_with_prompt(
|
|
|
42
39
|
working_directory: Path,
|
|
43
40
|
timeout_seconds: int = DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
44
41
|
) -> None:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
prompt_stdin=prompt_stdin,
|
|
52
|
-
)
|
|
53
|
-
finally:
|
|
54
|
-
prompt_stdin.close()
|
|
42
|
+
dispatcher._run_claude_with_headless_overrides(
|
|
43
|
+
[],
|
|
44
|
+
timeout_seconds=timeout_seconds,
|
|
45
|
+
working_directory=working_directory,
|
|
46
|
+
prompt_text=prompt_file.read_text(encoding=UTF8_ENCODING),
|
|
47
|
+
)
|
|
55
48
|
|
|
56
49
|
|
|
57
50
|
def test_forwards_text_codec_keywords_to_subprocess_runner(
|
|
@@ -61,10 +54,6 @@ def test_forwards_text_codec_keywords_to_subprocess_runner(
|
|
|
61
54
|
prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding=UTF8_ENCODING)
|
|
62
55
|
all_observed_runner_keywords = _install_dispatcher_codec_seams(
|
|
63
56
|
monkeypatch,
|
|
64
|
-
all_chain_codec_keywords={
|
|
65
|
-
FIXTURE_ENCODING_KEYWORD_NAME: FIXTURE_CHAIN_ENCODING,
|
|
66
|
-
FIXTURE_ERRORS_KEYWORD_NAME: FIXTURE_CHAIN_ERRORS,
|
|
67
|
-
},
|
|
68
57
|
)
|
|
69
58
|
|
|
70
59
|
_run_headless_with_prompt(
|
|
@@ -82,14 +71,13 @@ def test_forwards_text_codec_keywords_to_subprocess_runner(
|
|
|
82
71
|
)
|
|
83
72
|
|
|
84
73
|
|
|
85
|
-
def
|
|
74
|
+
def test_prompt_and_cwd_reach_broker(
|
|
86
75
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
87
76
|
) -> None:
|
|
88
77
|
prompt_file = tmp_path / "prompt.txt"
|
|
89
78
|
prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding=UTF8_ENCODING)
|
|
90
79
|
all_observed_runner_keywords = _install_dispatcher_codec_seams(
|
|
91
80
|
monkeypatch,
|
|
92
|
-
all_chain_codec_keywords={},
|
|
93
81
|
)
|
|
94
82
|
|
|
95
83
|
_run_headless_with_prompt(
|
|
@@ -97,5 +85,5 @@ def test_absent_text_codec_keywords_are_not_invented(
|
|
|
97
85
|
working_directory=tmp_path,
|
|
98
86
|
)
|
|
99
87
|
|
|
100
|
-
assert
|
|
101
|
-
assert
|
|
88
|
+
assert all_observed_runner_keywords["stdin_text"] == FIXTURE_PROMPT_TEXT
|
|
89
|
+
assert all_observed_runner_keywords["cwd"] == tmp_path
|
|
@@ -23,7 +23,7 @@ def _permission_mode_seen_by(
|
|
|
23
23
|
monkeypatch: pytest.MonkeyPatch, effective_user_id: int
|
|
24
24
|
) -> str:
|
|
25
25
|
"""Read the review permission mode a caller with this user id resolves."""
|
|
26
|
-
monkeypatch.setattr(os, "geteuid", lambda: effective_user_id)
|
|
26
|
+
monkeypatch.setattr(os, "geteuid", lambda: effective_user_id, raising=False)
|
|
27
27
|
reloaded_constants = importlib.reload(code_review_constants)
|
|
28
28
|
return str(reloaded_constants.REVIEW_PERMISSION_MODE)
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ def _permission_mode_seen_by(
|
|
|
31
31
|
def test_root_detection_agrees_with_this_process(
|
|
32
32
|
monkeypatch: pytest.MonkeyPatch,
|
|
33
33
|
) -> None:
|
|
34
|
-
monkeypatch.setattr(os, "geteuid", lambda: ROOT_USER_ID)
|
|
34
|
+
monkeypatch.setattr(os, "geteuid", lambda: ROOT_USER_ID, raising=False)
|
|
35
35
|
reloaded_constants = importlib.reload(code_review_constants)
|
|
36
36
|
|
|
37
37
|
assert reloaded_constants.IS_ROOT_CALLER is True
|