claude-dev-env 8.43.5 → 8.43.6

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.
Files changed (32) hide show
  1. package/_shared/advisor/advisor-protocol.md +2 -2
  2. package/_shared/advisor/reference/cli-chain.md +9 -12
  3. package/_shared/advisor/reference/lifecycle.md +1 -1
  4. package/_shared/advisor/reference/third-party-bind.md +3 -3
  5. package/bin/install.test.mjs +1 -1
  6. package/docs/second-claude-account.md +11 -13
  7. package/package.json +1 -1
  8. package/scripts/_code_review_test_support.py +29 -39
  9. package/scripts/claude_account_worker.py +46 -297
  10. package/scripts/claude_account_worker_process.py +18 -154
  11. package/scripts/claude_account_worker_report.py +8 -4
  12. package/scripts/claude_chain_usage.py +7 -271
  13. package/scripts/codec_forwarding_test_support.py +11 -65
  14. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
  15. package/scripts/invoke_code_review.py +34 -56
  16. package/scripts/resolve_worker_spawn.py +40 -72
  17. package/scripts/test_account_broker_guard.py +0 -4
  18. package/scripts/test_claude_account_worker.py +105 -350
  19. package/scripts/test_claude_chain_usage.py +50 -510
  20. package/scripts/test_dispatcher_profile_import.py +5 -9
  21. package/scripts/test_invoke_code_review.py +7 -6
  22. package/scripts/test_invoke_code_review_chain.py +26 -0
  23. package/scripts/test_invoke_code_review_cli.py +8 -8
  24. package/scripts/test_invoke_code_review_codec.py +3 -11
  25. package/scripts/test_invoke_code_review_contract.py +5 -1
  26. package/scripts/test_resolve_worker_spawn.py +118 -265
  27. package/scripts/test_resolve_worker_spawn_codec.py +9 -21
  28. package/scripts/tests/test_code_review_constants.py +2 -2
  29. package/scripts/claude_account_choice.py +0 -422
  30. package/scripts/claude_chain_runner.py +0 -1289
  31. package/scripts/test_claude_account_choice.py +0 -355
  32. package/scripts/test_claude_chain_runner.py +0 -1839
@@ -1,446 +1,201 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- import subprocess
5
- from collections.abc import Callable
6
- from datetime import datetime, timedelta, timezone
4
+ from datetime import datetime, timezone
7
5
  from pathlib import Path
8
- from typing import Any
9
6
 
10
7
  import pytest
11
8
 
12
- import claude_account_choice
13
- import claude_account_worker
14
- from claude_chain_usage import AccountUsageMeters
15
- from claude_account_worker import (
16
- AccountSelection,
17
- WorkerDependencies,
18
- build_argument_parser,
19
- select_account,
20
- run_worker,
21
- )
22
- from dev_env_scripts_constants.claude_account_worker_constants import (
23
- CHOICE_MAIN,
24
- CHOICE_SECOND,
25
- CHOICE_WAIT,
26
- CLAUDE_CONFIG_DIR_ENV_VAR,
27
- STDOUT_TAIL_CHARACTER_LIMIT,
28
- TIMEOUT_EXIT_CODE,
9
+ import claude_account_worker as worker
10
+ from dev_env_scripts_constants.account_broker_constants import (
11
+ BrokerConfigurationError,
12
+ JobOutcome,
13
+ Product,
29
14
  WAIT_EXIT_CODE,
30
15
  )
31
-
32
- NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
33
-
34
-
35
- class FakeProcess:
36
- def __init__(
37
- self,
38
- *,
39
- stdout: str = '{"result":"ready","is_error":false}',
40
- returncode: int = 0,
41
- timeout_first: bool = False,
42
- timeout_output: str = "",
43
- drain_timeout_count: int = 0,
44
- ) -> None:
45
- self.stdout = stdout
46
- self.returncode = returncode
47
- self.timeout_first = timeout_first
48
- self.timeout_output = timeout_output
49
- self.drain_timeout_count = drain_timeout_count
50
- self.pid = 42
51
- self.communicate_calls: list[tuple[str | None, float | None]] = []
52
- self.terminated = False
53
-
54
- def communicate(
55
- self,
56
- input: str | None = None,
57
- timeout: float | None = None,
58
- ) -> tuple[str, str]:
59
- self.communicate_calls.append((input, timeout))
60
- if self.timeout_first and len(self.communicate_calls) == 1:
61
- raise subprocess.TimeoutExpired(
62
- cmd="claude",
63
- timeout=timeout,
64
- output=self.timeout_output,
65
- )
66
- if self.drain_timeout_count:
67
- self.drain_timeout_count -= 1
68
- raise subprocess.TimeoutExpired(
69
- cmd="claude",
70
- timeout=timeout,
71
- output=self.timeout_output,
72
- )
73
- return self.stdout, ""
74
-
75
- def poll(self) -> int | None:
76
- return self.returncode if self.terminated else None
77
-
78
- def kill(self) -> None:
79
- self.terminated = True
80
- self.returncode = -9
81
-
82
-
83
- def _selection(
84
- account: str,
85
- *,
86
- config_dir: Path | None = None,
87
- reason: str = "selected by test",
88
- ) -> AccountSelection:
89
- return AccountSelection(account=account, config_dir=config_dir, reason=reason)
16
+ from dev_env_scripts_constants.claude_account_worker_constants import STDOUT_TAIL_CHARACTER_LIMIT
90
17
 
91
18
 
92
- def _make_runner(
93
- process: FakeProcess,
94
- captured: dict[str, Any],
95
- ) -> Callable[..., FakeProcess]:
96
- def run(arguments: list[str], **options: Any) -> FakeProcess:
97
- captured["arguments"] = arguments
98
- captured.update(options)
99
- return process
100
-
101
- return run
19
+ def _outcome(
20
+ *,
21
+ stdout: str = '{"result":"ready","is_error":false}',
22
+ returncode: int = 0,
23
+ account_name: str | None = "extra_2",
24
+ status: str = "served",
25
+ wait_reset_at: datetime | None = None,
26
+ ) -> JobOutcome:
27
+ return JobOutcome(
28
+ returncode,
29
+ stdout,
30
+ "",
31
+ account_name,
32
+ ((account_name, status),) if account_name else (),
33
+ status,
34
+ None,
35
+ wait_reset_at,
36
+ )
102
37
 
103
38
 
104
39
  def _run_worker(
40
+ monkeypatch: pytest.MonkeyPatch,
105
41
  tmp_path: Path,
42
+ outcome: JobOutcome | Exception,
106
43
  *,
107
- selection: AccountSelection,
108
- process: FakeProcess,
109
- parent_environment: dict[str, str] | None = None,
110
- process_terminator: Callable[[FakeProcess], None] | None = None,
111
44
  model: str | None = None,
112
45
  permission_mode: str = "auto",
113
- ) -> tuple[int, dict[str, Any], dict[str, Any]]:
46
+ ) -> tuple[int, dict[str, object], dict[str, object]]:
114
47
  prompt_file = tmp_path / "brief.md"
115
48
  prompt_file.write_text("standalone brief", encoding="utf-8")
116
49
  report_file = tmp_path / "report.json"
117
- captured: dict[str, Any] = {}
118
- runner = _make_runner(process, captured)
119
- dependencies = WorkerDependencies(
120
- process_factory=runner,
121
- all_parent_environment_variables=(
122
- {"KEEP_ME": "present"} if parent_environment is None else parent_environment
123
- ),
124
- binary_resolver=lambda _: "claude.exe",
125
- process_terminator=(
126
- process_terminator if process_terminator is not None else lambda _: None
127
- ),
128
- monotonic_clock=lambda: 1.0,
129
- )
130
- exit_code = run_worker(
50
+ captured: dict[str, object] = {}
51
+
52
+ def run_job(product: Product, argv: list[str], **options: object) -> JobOutcome:
53
+ captured.update(product=product, argv=argv, **options)
54
+ if isinstance(outcome, Exception):
55
+ raise outcome
56
+ return outcome
57
+
58
+ monkeypatch.setattr(worker, "worker_job_runner", run_job)
59
+ exit_code = worker.run_worker(
131
60
  prompt_file=prompt_file,
132
61
  cwd=tmp_path,
133
62
  report_file=report_file,
134
63
  model=model,
135
64
  permission_mode=permission_mode,
136
65
  timeout_minutes=60,
137
- decision_selector=lambda: selection,
138
- dependencies=dependencies,
139
66
  )
140
67
  return exit_code, json.loads(report_file.read_text(encoding="utf-8")), captured
141
68
 
142
69
 
143
- def test_should_set_second_profile_and_pass_parent_environment(
144
- tmp_path: Path,
70
+ def test_should_send_prompt_and_flags_to_broker(
71
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
145
72
  ) -> None:
146
- profile_dir = tmp_path / "second-profile"
147
- process = FakeProcess()
148
73
  exit_code, report, captured = _run_worker(
149
- tmp_path,
150
- selection=_selection(CHOICE_SECOND, config_dir=profile_dir),
151
- process=process,
152
- parent_environment={
153
- CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
154
- "KEEP_ME": "present",
155
- },
156
- model="sonnet",
157
- permission_mode="plan",
74
+ monkeypatch, tmp_path, _outcome(), model="sonnet", permission_mode="plan"
158
75
  )
159
76
 
160
77
  assert exit_code == 0
161
- assert report["account"] == CHOICE_SECOND
162
- assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
163
- assert captured["env"]["KEEP_ME"] == "present"
164
- assert captured["arguments"] == [
165
- "claude.exe",
166
- "-p",
167
- "--output-format",
168
- "json",
169
- "--permission-mode",
170
- "plan",
171
- "--model",
172
- "sonnet",
78
+ assert report["account"] == "extra_2"
79
+ assert report["result"] == "ready"
80
+ assert captured["product"] is Product.CLAUDE
81
+ assert captured["argv"] == [
82
+ "claude", "-p", "--output-format", "json", "--permission-mode", "plan", "--model", "sonnet"
173
83
  ]
174
- assert captured["cwd"] == str(tmp_path)
175
- assert process.communicate_calls[0][0] == "standalone brief"
84
+ assert captured["stdin_text"] == "standalone brief"
85
+ assert captured["cwd"] == tmp_path
86
+ assert captured["timeout_seconds"] == 3600
176
87
 
177
88
 
178
- def test_should_remove_inherited_profile_for_main_account(tmp_path: Path) -> None:
179
- process = FakeProcess()
89
+ def test_should_report_wait_with_reset_and_exit_three(
90
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
91
+ ) -> None:
92
+ reset_at = datetime(2026, 10, 4, 12, tzinfo=timezone.utc)
180
93
  exit_code, report, captured = _run_worker(
94
+ monkeypatch,
181
95
  tmp_path,
182
- selection=_selection(CHOICE_MAIN, config_dir=tmp_path / "main-profile"),
183
- process=process,
184
- parent_environment={
185
- CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
186
- "KEEP_ME": "present",
187
- },
188
- )
189
-
190
- assert exit_code == 0
191
- assert report["account"] == CHOICE_MAIN
192
- assert CLAUDE_CONFIG_DIR_ENV_VAR not in captured["env"]
193
- assert captured["env"]["KEEP_ME"] == "present"
194
-
195
-
196
- def test_should_write_wait_reason_and_start_no_process(tmp_path: Path) -> None:
197
- report_file = tmp_path / "wait-report.json"
198
- process_started = False
199
-
200
- def forbidden_runner(*_: Any, **__: Any) -> FakeProcess:
201
- nonlocal process_started
202
- process_started = True
203
- raise AssertionError("wait must not start a process")
204
-
205
- exit_code = run_worker(
206
- prompt_file=tmp_path / "missing-brief.md",
207
- cwd=tmp_path,
208
- report_file=report_file,
209
- model=None,
210
- permission_mode="auto",
211
- timeout_minutes=60,
212
- decision_selector=lambda: _selection(
213
- CHOICE_WAIT,
214
- reason="second account resets at 2026-09-24T12:00:00Z",
215
- ),
216
- dependencies=WorkerDependencies(
217
- process_factory=forbidden_runner,
218
- all_parent_environment_variables={},
219
- binary_resolver=lambda _: (_ for _ in ()).throw(
220
- AssertionError("wait must not resolve a binary")
221
- ),
222
- process_terminator=lambda _: None,
223
- monotonic_clock=lambda: 1.0,
224
- ),
96
+ _outcome(stdout="", returncode=WAIT_EXIT_CODE, account_name=None, status="wait", wait_reset_at=reset_at),
225
97
  )
226
- report = json.loads(report_file.read_text(encoding="utf-8"))
227
98
 
228
99
  assert exit_code == WAIT_EXIT_CODE
229
- assert process_started is False
230
- assert report["account"] == CHOICE_WAIT
231
- assert report["reason"] == "second account resets at 2026-09-24T12:00:00Z"
232
- assert report["is_error"] is True
100
+ assert report["account"] == "wait"
101
+ assert report["wait_reset_at"] == reset_at.isoformat()
102
+ assert report["reason"] == f"no account has room; next reset at {reset_at.isoformat()}"
103
+ assert report["is_error"] is False
104
+ assert captured["product"] is Product.CLAUDE
233
105
 
234
106
 
235
- def test_should_kill_child_on_timeout_and_exit_124(tmp_path: Path) -> None:
236
- process = FakeProcess(
237
- stdout='{"result":"partial"}',
238
- returncode=-9,
239
- timeout_first=True,
240
- timeout_output='{"result":"partial"}',
241
- )
242
- termination_calls: list[FakeProcess] = []
107
+ def test_should_preserve_exhausted_wait_exit_code(
108
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
109
+ ) -> None:
243
110
  exit_code, report, _ = _run_worker(
111
+ monkeypatch,
244
112
  tmp_path,
245
- selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
246
- process=process,
247
- process_terminator=lambda child: termination_calls.append(child),
113
+ _outcome(stdout="", returncode=WAIT_EXIT_CODE, account_name=None, status="exhausted"),
248
114
  )
249
115
 
250
- assert exit_code == TIMEOUT_EXIT_CODE
251
- assert termination_calls == [process]
252
- assert len(process.communicate_calls) == 2
253
- assert report["exit_code"] == TIMEOUT_EXIT_CODE
254
- assert report["is_error"] is True
116
+ assert exit_code == WAIT_EXIT_CODE
117
+ assert report["reason"] == "no account has room"
255
118
 
256
119
 
257
- def test_should_copy_parsed_result_into_report(tmp_path: Path) -> None:
258
- process = FakeProcess(stdout='{"result":"ready","is_error":false}')
120
+ def test_should_copy_json_error_flag_into_report(
121
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
122
+ ) -> None:
259
123
  exit_code, report, _ = _run_worker(
260
- tmp_path,
261
- selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
262
- process=process,
124
+ monkeypatch, tmp_path, _outcome(stdout='{"result":"failed","is_error":true}')
263
125
  )
264
126
 
265
127
  assert exit_code == 0
266
- assert report["result"] == "ready"
267
- assert report["is_error"] is False
128
+ assert report["result"] == "failed"
129
+ assert report["is_error"] is True
268
130
 
269
131
 
270
- def test_should_write_raw_stdout_tail_when_output_is_not_json(tmp_path: Path) -> None:
271
- stdout_text = "x" * (STDOUT_TAIL_CHARACTER_LIMIT - 4) + "tail"
272
- process = FakeProcess(stdout=stdout_text)
273
- exit_code, report, _ = _run_worker(
274
- tmp_path,
275
- selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
276
- process=process,
277
- )
132
+ def test_should_keep_stdout_tail_when_output_is_not_json(
133
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
134
+ ) -> None:
135
+ stdout_text = "x" * (STDOUT_TAIL_CHARACTER_LIMIT + 4)
136
+ exit_code, report, _ = _run_worker(monkeypatch, tmp_path, _outcome(stdout=stdout_text))
278
137
 
279
138
  assert exit_code == 0
280
139
  assert report["result"] == stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
281
140
  assert report["is_error"] is True
282
141
 
283
142
 
284
- def test_should_preserve_child_exit_code_three_for_selected_account(
285
- tmp_path: Path,
143
+ def test_should_preserve_served_failure_code(
144
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
286
145
  ) -> None:
287
- process = FakeProcess(returncode=3)
288
146
  exit_code, report, _ = _run_worker(
289
- tmp_path,
290
- selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
291
- process=process,
147
+ monkeypatch, tmp_path, _outcome(returncode=3, status="advisor_blocked")
292
148
  )
293
149
 
294
150
  assert exit_code == 3
295
- assert report["account"] == CHOICE_SECOND
296
- assert report["exit_code"] == 3
297
- assert report["is_error"] is True
298
-
299
-
300
- def test_should_keep_claude_json_error_flag(tmp_path: Path) -> None:
301
- process = FakeProcess(stdout='{"result":"failed","is_error":true}')
302
- exit_code, report, _ = _run_worker(
303
- tmp_path,
304
- selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
305
- process=process,
306
- )
307
-
308
- assert exit_code == 0
309
- assert report["result"] == "failed"
151
+ assert report["account"] == "extra_2"
310
152
  assert report["is_error"] is True
311
153
 
312
154
 
313
- def test_should_pick_second_account_when_neither_meter_is_readable(
155
+ def test_should_write_report_for_unreadable_prompt(
314
156
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
315
157
  ) -> None:
316
- main_home = tmp_path / "main-home"
317
- second_home = tmp_path / "second-home"
318
- monkeypatch.setattr(Path, "home", lambda: main_home)
319
- monkeypatch.setattr(claude_account_worker, "default_profile_home", lambda: second_home)
320
-
321
- selection = select_account()
322
-
323
- assert selection.account == CHOICE_SECOND
324
- assert selection.config_dir == second_home
325
-
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
158
  report_file = tmp_path / "report.json"
159
+ monkeypatch.setattr(worker, "worker_job_runner", lambda *args, **kwargs: pytest.fail("broker ran"))
388
160
 
389
- exit_code = run_worker(
390
- prompt_file=tmp_path / "unused.md",
161
+ exit_code = worker.run_worker(
162
+ prompt_file=tmp_path / "missing.md",
391
163
  cwd=tmp_path,
392
164
  report_file=report_file,
393
165
  model=None,
394
166
  permission_mode="auto",
395
167
  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
168
  )
407
- report = json.loads(report_file.read_text(encoding="utf-8"))
408
169
 
409
- assert exit_code == WAIT_EXIT_CODE
410
- assert report["account"] == CHOICE_WAIT
411
- assert (NOW + timedelta(days=2)).isoformat() in report["reason"]
170
+ report = json.loads(report_file.read_text(encoding="utf-8"))
171
+ assert exit_code == report["exit_code"]
172
+ assert report["reason"] == "prompt file is unreadable"
412
173
 
413
174
 
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(),
175
+ def test_should_report_broker_configuration_error(
176
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
177
+ ) -> None:
178
+ exit_code, report, _ = _run_worker(
179
+ monkeypatch, tmp_path, BrokerConfigurationError("account list is invalid")
420
180
  )
421
181
 
422
- assert exit_code == 0
423
- assert report["account"] == "extra_2"
424
- assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
182
+ assert exit_code == 2
183
+ assert report["reason"] == "account list is invalid"
425
184
 
426
185
 
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")
186
+ def test_should_ignore_extra_profile_list(
187
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
188
+ ) -> None:
189
+ (tmp_path / "extra-profiles.json").write_text("{", encoding="utf-8")
435
190
 
436
- with pytest.raises(ValueError):
437
- claude_account_worker.extra_config_directories(main_home)
191
+ exit_code, report, _ = _run_worker(monkeypatch, tmp_path, _outcome())
438
192
 
193
+ assert exit_code == 0
194
+ assert report["account"] == "extra_2"
439
195
 
440
- def test_should_require_prompt_file_and_report_file_flags() -> None:
441
- parser = build_argument_parser()
442
196
 
443
- arguments = parser.parse_args(
197
+ def test_should_require_prompt_and_report_flags() -> None:
198
+ arguments = worker.build_argument_parser().parse_args(
444
199
  ["--prompt-file", "brief.md", "--report-file", "report.json"]
445
200
  )
446
201