claude-dev-env 2.7.0 → 2.8.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/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/pr-loop/worker-spawn.md +3 -1
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +4 -4
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
- package/scripts/grok_headless_runner.py +148 -18
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +81 -23
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +547 -10
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +185 -15
- package/scripts/test_spawn_grok_batch.py +326 -22
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
- package/skills/autoconverge/workflow/converge.mjs +113 -31
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +10 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -24,11 +24,13 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
|
24
24
|
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
25
25
|
ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
|
|
26
26
|
ATTEMPT_SUMMARY_JOIN_SEPARATOR,
|
|
27
|
+
CHAIN_ADVISOR_BLOCKED_EXIT_CODE,
|
|
27
28
|
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
28
29
|
CHAIN_EXHAUSTED_EXIT_CODE,
|
|
29
30
|
CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
|
|
30
31
|
CLAUDE_HOME_SUBDIRECTORY,
|
|
31
32
|
CLI_ARGUMENTS_SEPARATOR,
|
|
33
|
+
CLI_ROUTING_MODE_FLAG,
|
|
32
34
|
CLI_TIMEOUT_FLAG,
|
|
33
35
|
CODEC_ERROR_STRATEGY,
|
|
34
36
|
CONFIG_CHAIN_EMPTY_REASON,
|
|
@@ -48,6 +50,11 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
|
48
50
|
DEFAULT_TIMEOUT_SECONDS,
|
|
49
51
|
EXAMPLE_CONFIG_FILENAME,
|
|
50
52
|
NO_COMPLETED_PROCESS_RETURN_CODE,
|
|
53
|
+
ROUTING_MODE_ORDERED_ACCOUNT,
|
|
54
|
+
SESSION_ID_JSON_KEY,
|
|
55
|
+
TERMINAL_STATUS_ADVISOR_BLOCKED,
|
|
56
|
+
TERMINAL_STATUS_SERVED,
|
|
57
|
+
TERMINAL_STATUS_TIMEOUT,
|
|
51
58
|
UTF8_ENCODING,
|
|
52
59
|
)
|
|
53
60
|
|
|
@@ -65,6 +72,15 @@ _EQUAL_WEEKLY_REMAINING_PERCENT = 50.0
|
|
|
65
72
|
_HIGH_WEEKLY_REMAINING_PERCENT = 90.0
|
|
66
73
|
_MID_WEEKLY_REMAINING_PERCENT = 50.0
|
|
67
74
|
_LOW_WEEKLY_REMAINING_PERCENT = 10.0
|
|
75
|
+
_PRIMARY_LAUNCHER = "primary-launcher"
|
|
76
|
+
_SECONDARY_LAUNCHER = "secondary-launcher"
|
|
77
|
+
_BOUND_SESSION_ID = "session-bound-abc-123"
|
|
78
|
+
_AUTHENTICATION_ERROR_STDERR = "authentication_error: invalid API key"
|
|
79
|
+
_GENERIC_PROCESS_ERROR_STDERR = "unknown flag: --not-a-real-flag"
|
|
80
|
+
_JSON_BIND_STDOUT_WITH_SESSION = (
|
|
81
|
+
f'{{"type":"result","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}",'
|
|
82
|
+
f'"result":"ENDORSE"}}\n'
|
|
83
|
+
)
|
|
68
84
|
|
|
69
85
|
|
|
70
86
|
def _completed(command, returncode, stdout="", stderr=""):
|
|
@@ -567,6 +583,7 @@ def test_timeout_on_primary_does_not_fall_over(
|
|
|
567
583
|
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
568
584
|
assert chain_result.served_command is None
|
|
569
585
|
assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
|
|
586
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_TIMEOUT
|
|
570
587
|
assert len(chain_result.attempts) == 1
|
|
571
588
|
assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
|
|
572
589
|
|
|
@@ -1119,3 +1136,344 @@ def test_cli_emits_utf8_when_console_encoding_is_legacy(tmp_path: Path) -> None:
|
|
|
1119
1136
|
assert completed.returncode == 0
|
|
1120
1137
|
assert b"Traceback" not in completed.stderr
|
|
1121
1138
|
assert "report ✅" in completed.stdout.decode(UTF8_ENCODING)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def test_usage_limit_classifier_keys_on_exact_signature_constants() -> None:
|
|
1142
|
+
assert ALL_USAGE_LIMIT_SIGNATURES == (
|
|
1143
|
+
"hit your session limit",
|
|
1144
|
+
"usage limit reached",
|
|
1145
|
+
"out of usage",
|
|
1146
|
+
"usage quota exceeded",
|
|
1147
|
+
)
|
|
1148
|
+
assert _A_SIGNATURE in ALL_USAGE_LIMIT_SIGNATURES
|
|
1149
|
+
assert runner._is_usage_limit_failure(
|
|
1150
|
+
_completed("primary-launcher", 1, stderr=_A_SIGNATURE)
|
|
1151
|
+
)
|
|
1152
|
+
assert not runner._is_usage_limit_failure(
|
|
1153
|
+
_completed("primary-launcher", 1, stderr=_GENERIC_PROCESS_ERROR_STDERR)
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def test_extract_session_id_from_stdout_reads_json_object_and_ndjson() -> None:
|
|
1158
|
+
single_object = (
|
|
1159
|
+
f'{{"type":"result","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}",'
|
|
1160
|
+
f'"result":"ENDORSE"}}'
|
|
1161
|
+
)
|
|
1162
|
+
assert runner.extract_session_id_from_stdout(single_object) == _BOUND_SESSION_ID
|
|
1163
|
+
ndjson_stream = (
|
|
1164
|
+
f'{{"type":"system","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}"}}\n'
|
|
1165
|
+
f'{{"type":"result","{SESSION_ID_JSON_KEY}":"{_BOUND_SESSION_ID}",'
|
|
1166
|
+
f'"result":"ok"}}\n'
|
|
1167
|
+
)
|
|
1168
|
+
assert runner.extract_session_id_from_stdout(ndjson_stream) == _BOUND_SESSION_ID
|
|
1169
|
+
assert runner.extract_session_id_from_stdout("not json at all") is None
|
|
1170
|
+
assert runner.extract_session_id_from_stdout("") is None
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def test_ordered_account_mode_tries_primary_launcher_first(
|
|
1174
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1175
|
+
) -> None:
|
|
1176
|
+
config_file = _write_chain_config(
|
|
1177
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1178
|
+
)
|
|
1179
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
1180
|
+
{
|
|
1181
|
+
_PRIMARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
|
|
1182
|
+
_SECONDARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
1183
|
+
}
|
|
1184
|
+
)
|
|
1185
|
+
recorder = _install(
|
|
1186
|
+
monkeypatch,
|
|
1187
|
+
config_file,
|
|
1188
|
+
{
|
|
1189
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1190
|
+
_PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
|
|
1191
|
+
),
|
|
1192
|
+
_SECONDARY_LAUNCHER: _completed(
|
|
1193
|
+
_SECONDARY_LAUNCHER, 0, stdout="should-not-run"
|
|
1194
|
+
),
|
|
1195
|
+
},
|
|
1196
|
+
weekly_usage_reporter=usage_reporter,
|
|
1197
|
+
)
|
|
1198
|
+
chain_result = runner.run_claude(
|
|
1199
|
+
_PROMPT_ARGUMENTS,
|
|
1200
|
+
timeout_seconds=5,
|
|
1201
|
+
routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1202
|
+
)
|
|
1203
|
+
assert chain_result.served_command == _PRIMARY_LAUNCHER
|
|
1204
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
|
|
1205
|
+
assert recorder.invocations[0][0] == _PRIMARY_LAUNCHER
|
|
1206
|
+
assert usage_reporter.call_count["count"] == 0
|
|
1207
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
1208
|
+
_PRIMARY_LAUNCHER
|
|
1209
|
+
]
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def test_ordered_account_mode_usage_limit_falls_over_to_secondary(
|
|
1213
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1214
|
+
) -> None:
|
|
1215
|
+
config_file = _write_chain_config(
|
|
1216
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1217
|
+
)
|
|
1218
|
+
_install(
|
|
1219
|
+
monkeypatch,
|
|
1220
|
+
config_file,
|
|
1221
|
+
{
|
|
1222
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1223
|
+
_PRIMARY_LAUNCHER, 1, stderr=_A_SIGNATURE
|
|
1224
|
+
),
|
|
1225
|
+
_SECONDARY_LAUNCHER: _completed(
|
|
1226
|
+
_SECONDARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
|
|
1227
|
+
),
|
|
1228
|
+
},
|
|
1229
|
+
)
|
|
1230
|
+
chain_result = runner.run_claude(
|
|
1231
|
+
_PROMPT_ARGUMENTS,
|
|
1232
|
+
timeout_seconds=5,
|
|
1233
|
+
routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1234
|
+
)
|
|
1235
|
+
assert chain_result.served_command == _SECONDARY_LAUNCHER
|
|
1236
|
+
assert chain_result.returncode == 0
|
|
1237
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
|
|
1238
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
1239
|
+
_PRIMARY_LAUNCHER,
|
|
1240
|
+
_SECONDARY_LAUNCHER,
|
|
1241
|
+
]
|
|
1242
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
|
|
1243
|
+
assert chain_result.attempts[1].status == ATTEMPT_STATUS_SERVED
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
def test_ordered_account_mode_authentication_error_is_advisor_blocked(
|
|
1247
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1248
|
+
) -> None:
|
|
1249
|
+
config_file = _write_chain_config(
|
|
1250
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1251
|
+
)
|
|
1252
|
+
recorder = _install(
|
|
1253
|
+
monkeypatch,
|
|
1254
|
+
config_file,
|
|
1255
|
+
{
|
|
1256
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1257
|
+
_PRIMARY_LAUNCHER, 1, stderr=_AUTHENTICATION_ERROR_STDERR
|
|
1258
|
+
),
|
|
1259
|
+
_SECONDARY_LAUNCHER: _completed(
|
|
1260
|
+
_SECONDARY_LAUNCHER, 0, stdout="should-not-run"
|
|
1261
|
+
),
|
|
1262
|
+
},
|
|
1263
|
+
)
|
|
1264
|
+
chain_result = runner.run_claude(
|
|
1265
|
+
_PROMPT_ARGUMENTS,
|
|
1266
|
+
timeout_seconds=5,
|
|
1267
|
+
routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1268
|
+
)
|
|
1269
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED
|
|
1270
|
+
assert chain_result.served_command is None
|
|
1271
|
+
assert chain_result.returncode == 1
|
|
1272
|
+
assert _AUTHENTICATION_ERROR_STDERR in chain_result.stderr
|
|
1273
|
+
assert len(recorder.invocations) == 1
|
|
1274
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def test_ordered_account_mode_timeout_is_advisor_blocked(
|
|
1278
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1279
|
+
) -> None:
|
|
1280
|
+
config_file = _write_chain_config(
|
|
1281
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1282
|
+
)
|
|
1283
|
+
recorder = _install(
|
|
1284
|
+
monkeypatch,
|
|
1285
|
+
config_file,
|
|
1286
|
+
{
|
|
1287
|
+
_PRIMARY_LAUNCHER: subprocess.TimeoutExpired(
|
|
1288
|
+
cmd=[_PRIMARY_LAUNCHER], timeout=5
|
|
1289
|
+
),
|
|
1290
|
+
_SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
|
|
1291
|
+
},
|
|
1292
|
+
)
|
|
1293
|
+
chain_result = runner.run_claude(
|
|
1294
|
+
_PROMPT_ARGUMENTS,
|
|
1295
|
+
timeout_seconds=5,
|
|
1296
|
+
routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1297
|
+
)
|
|
1298
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED
|
|
1299
|
+
assert chain_result.served_command is None
|
|
1300
|
+
assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
|
|
1301
|
+
assert len(recorder.invocations) == 1
|
|
1302
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def test_ordered_account_mode_generic_process_error_is_advisor_blocked(
|
|
1306
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1307
|
+
) -> None:
|
|
1308
|
+
config_file = _write_chain_config(
|
|
1309
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1310
|
+
)
|
|
1311
|
+
recorder = _install(
|
|
1312
|
+
monkeypatch,
|
|
1313
|
+
config_file,
|
|
1314
|
+
{
|
|
1315
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1316
|
+
_PRIMARY_LAUNCHER, 2, stderr=_GENERIC_PROCESS_ERROR_STDERR
|
|
1317
|
+
),
|
|
1318
|
+
_SECONDARY_LAUNCHER: _completed(
|
|
1319
|
+
_SECONDARY_LAUNCHER, 0, stdout="should-not-run"
|
|
1320
|
+
),
|
|
1321
|
+
},
|
|
1322
|
+
)
|
|
1323
|
+
chain_result = runner.run_claude(
|
|
1324
|
+
_PROMPT_ARGUMENTS,
|
|
1325
|
+
timeout_seconds=5,
|
|
1326
|
+
routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1327
|
+
)
|
|
1328
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_ADVISOR_BLOCKED
|
|
1329
|
+
assert chain_result.served_command is None
|
|
1330
|
+
assert chain_result.returncode == 2
|
|
1331
|
+
assert len(recorder.invocations) == 1
|
|
1332
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def test_ordered_account_mode_configuration_error_does_not_fall_over(
|
|
1336
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
1337
|
+
) -> None:
|
|
1338
|
+
missing_config = tmp_path / CONFIG_FILENAME
|
|
1339
|
+
monkeypatch.setattr(runner, "chain_config_path", lambda: missing_config)
|
|
1340
|
+
_install_tty_stdin(monkeypatch)
|
|
1341
|
+
exit_code = runner.main(
|
|
1342
|
+
[
|
|
1343
|
+
CLI_ROUTING_MODE_FLAG,
|
|
1344
|
+
ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1345
|
+
CLI_ARGUMENTS_SEPARATOR,
|
|
1346
|
+
"-p",
|
|
1347
|
+
"hi",
|
|
1348
|
+
]
|
|
1349
|
+
)
|
|
1350
|
+
captured = capsys.readouterr()
|
|
1351
|
+
assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
|
|
1352
|
+
assert EXAMPLE_CONFIG_FILENAME in captured.err
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def test_ordered_account_mode_preserves_session_id_on_success(
|
|
1356
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1357
|
+
) -> None:
|
|
1358
|
+
config_file = _write_chain_config(
|
|
1359
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1360
|
+
)
|
|
1361
|
+
_install(
|
|
1362
|
+
monkeypatch,
|
|
1363
|
+
config_file,
|
|
1364
|
+
{
|
|
1365
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1366
|
+
_PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
|
|
1367
|
+
),
|
|
1368
|
+
_SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
|
|
1369
|
+
},
|
|
1370
|
+
)
|
|
1371
|
+
chain_result = runner.run_claude(
|
|
1372
|
+
_PROMPT_ARGUMENTS,
|
|
1373
|
+
timeout_seconds=5,
|
|
1374
|
+
routing_mode=ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1375
|
+
)
|
|
1376
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
|
|
1377
|
+
assert chain_result.session_id == _BOUND_SESSION_ID
|
|
1378
|
+
assert chain_result.served_command == _PRIMARY_LAUNCHER
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def test_default_routing_mode_remains_usage_ranked(
|
|
1382
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1383
|
+
) -> None:
|
|
1384
|
+
config_file = _write_chain_config(
|
|
1385
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1386
|
+
)
|
|
1387
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
1388
|
+
{
|
|
1389
|
+
_PRIMARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
|
|
1390
|
+
_SECONDARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
1391
|
+
}
|
|
1392
|
+
)
|
|
1393
|
+
recorder = _install(
|
|
1394
|
+
monkeypatch,
|
|
1395
|
+
config_file,
|
|
1396
|
+
{
|
|
1397
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1398
|
+
_PRIMARY_LAUNCHER, 0, stdout="from-primary"
|
|
1399
|
+
),
|
|
1400
|
+
_SECONDARY_LAUNCHER: _completed(
|
|
1401
|
+
_SECONDARY_LAUNCHER, 0, stdout="from-secondary"
|
|
1402
|
+
),
|
|
1403
|
+
},
|
|
1404
|
+
weekly_usage_reporter=usage_reporter,
|
|
1405
|
+
)
|
|
1406
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
1407
|
+
assert chain_result.served_command == _SECONDARY_LAUNCHER
|
|
1408
|
+
assert chain_result.terminal_status == TERMINAL_STATUS_SERVED
|
|
1409
|
+
assert recorder.invocations[0][0] == _SECONDARY_LAUNCHER
|
|
1410
|
+
assert usage_reporter.call_count["count"] == 1
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
def test_cli_ordered_account_routing_mode_flag(
|
|
1414
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1415
|
+
) -> None:
|
|
1416
|
+
config_file = _write_chain_config(
|
|
1417
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1418
|
+
)
|
|
1419
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
1420
|
+
{
|
|
1421
|
+
_PRIMARY_LAUNCHER: _LOW_WEEKLY_REMAINING_PERCENT,
|
|
1422
|
+
_SECONDARY_LAUNCHER: _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
1423
|
+
}
|
|
1424
|
+
)
|
|
1425
|
+
recorder = _install(
|
|
1426
|
+
monkeypatch,
|
|
1427
|
+
config_file,
|
|
1428
|
+
{
|
|
1429
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1430
|
+
_PRIMARY_LAUNCHER, 0, stdout=_JSON_BIND_STDOUT_WITH_SESSION
|
|
1431
|
+
),
|
|
1432
|
+
_SECONDARY_LAUNCHER: _completed(
|
|
1433
|
+
_SECONDARY_LAUNCHER, 0, stdout="should-not-run"
|
|
1434
|
+
),
|
|
1435
|
+
},
|
|
1436
|
+
weekly_usage_reporter=usage_reporter,
|
|
1437
|
+
)
|
|
1438
|
+
exit_code = runner.main(
|
|
1439
|
+
[
|
|
1440
|
+
CLI_ROUTING_MODE_FLAG,
|
|
1441
|
+
ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1442
|
+
CLI_ARGUMENTS_SEPARATOR,
|
|
1443
|
+
"-p",
|
|
1444
|
+
"hi",
|
|
1445
|
+
]
|
|
1446
|
+
)
|
|
1447
|
+
assert exit_code == 0
|
|
1448
|
+
assert recorder.invocations[0][0] == _PRIMARY_LAUNCHER
|
|
1449
|
+
assert usage_reporter.call_count["count"] == 0
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def test_cli_ordered_account_advisor_blocked_exit_code(
|
|
1453
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1454
|
+
) -> None:
|
|
1455
|
+
config_file = _write_chain_config(
|
|
1456
|
+
tmp_path, [_entry(_PRIMARY_LAUNCHER), _entry(_SECONDARY_LAUNCHER)]
|
|
1457
|
+
)
|
|
1458
|
+
_install(
|
|
1459
|
+
monkeypatch,
|
|
1460
|
+
config_file,
|
|
1461
|
+
{
|
|
1462
|
+
_PRIMARY_LAUNCHER: _completed(
|
|
1463
|
+
_PRIMARY_LAUNCHER, 2, stderr=_GENERIC_PROCESS_ERROR_STDERR
|
|
1464
|
+
),
|
|
1465
|
+
_SECONDARY_LAUNCHER: _completed(_SECONDARY_LAUNCHER, 0),
|
|
1466
|
+
},
|
|
1467
|
+
)
|
|
1468
|
+
exit_code = runner.main(
|
|
1469
|
+
[
|
|
1470
|
+
CLI_ROUTING_MODE_FLAG,
|
|
1471
|
+
ROUTING_MODE_ORDERED_ACCOUNT,
|
|
1472
|
+
CLI_ARGUMENTS_SEPARATOR,
|
|
1473
|
+
"-p",
|
|
1474
|
+
"hi",
|
|
1475
|
+
]
|
|
1476
|
+
)
|
|
1477
|
+
assert exit_code == CHAIN_ADVISOR_BLOCKED_EXIT_CODE
|
|
1478
|
+
|
|
1479
|
+
|