claude-dev-env 2.10.0 → 2.12.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.
Files changed (39) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/advisor/CLAUDE.md +3 -2
  3. package/_shared/advisor/advisor-protocol.md +74 -108
  4. package/_shared/advisor/reference/advisor-block.md +37 -0
  5. package/_shared/advisor/reference/cli-chain.md +45 -0
  6. package/_shared/advisor/reference/consult-format.md +41 -0
  7. package/_shared/advisor/reference/lifecycle.md +21 -0
  8. package/_shared/advisor/reference/sol-rung.md +34 -0
  9. package/_shared/advisor/reference/spawn-walk-log.md +31 -0
  10. package/_shared/advisor/reference/third-party-bind.md +30 -0
  11. package/_shared/advisor/reference/warm-up.md +34 -0
  12. package/_shared/advisor/scripts/codex_sol_advisor.py +514 -0
  13. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +33 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
  17. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +636 -0
  18. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
  19. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
  20. package/_shared/advisor/scripts/tier_model_ids.py +24 -0
  21. package/commands/CLAUDE.md +1 -0
  22. package/commands/sr-loop.md +48 -0
  23. package/docs/references/CLAUDE.md +2 -1
  24. package/docs/references/advisor-tool.md +26 -8
  25. package/docs/references/team-advisor-skill.md +3 -3
  26. package/docs/references/weak-executor-advisor.md +91 -0
  27. package/hooks/blocking/test_fable_spawn_gate.py +18 -11
  28. package/package.json +1 -1
  29. package/skills/_shared/advisor/CLAUDE.md +1 -1
  30. package/skills/_shared/advisor/scripts/README.md +2 -0
  31. package/skills/grokify/SKILL.md +1 -1
  32. package/skills/grokify/templates/handoff-template.md +2 -2
  33. package/skills/orchestrator/SKILL.md +5 -4
  34. package/skills/team-advisor/SKILL.md +7 -4
  35. package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
  36. package/skills/usage-pause/SKILL.md +1 -1
  37. package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
  38. package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
  39. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
@@ -0,0 +1,636 @@
1
+ """Behavioral tests for the executable Codex Sol advisor path."""
2
+
3
+ import io
4
+ import importlib.util
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+ _ProcessRunner = Callable[..., subprocess.CompletedProcess[str]]
15
+
16
+
17
+ def _load_sol_module() -> ModuleType:
18
+ scripts_root = Path(__file__).parent.parent
19
+ config_root = scripts_root / "config"
20
+ sys.path.insert(0, str(config_root))
21
+ specification = importlib.util.spec_from_file_location(
22
+ "codex_sol_advisor", scripts_root / "codex_sol_advisor.py"
23
+ )
24
+ assert specification is not None and specification.loader is not None
25
+ module = importlib.util.module_from_spec(specification)
26
+ sys.modules[specification.name] = module
27
+ specification.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ sol_advisor = _load_sol_module()
32
+ SCRIPTS_ROOT = Path(__file__).parent.parent
33
+ USAGE_PROBE_PATH = (
34
+ SCRIPTS_ROOT.parents[2]
35
+ / "skills"
36
+ / "codex-review"
37
+ / "scripts"
38
+ / "codex_usage_probe.py"
39
+ )
40
+ WINDOWS_SHIM_PATH = r"C:\Users\me\AppData\Roaming\npm\codex.cmd"
41
+ ENABLED_SETTING_BY_NAME = {sol_advisor.SOL_ENV_VAR: "1"}
42
+
43
+
44
+ @pytest.fixture(autouse=True)
45
+ def _codex_on_search_path(monkeypatch: pytest.MonkeyPatch) -> None:
46
+ monkeypatch.setattr(
47
+ sol_advisor.shutil,
48
+ "which",
49
+ lambda name: "codex" if name == sol_advisor.CODEX_EXECUTABLE else None,
50
+ )
51
+
52
+
53
+ def _probe_process(
54
+ payload: object,
55
+ returncode: int = 0,
56
+ ) -> subprocess.CompletedProcess[str]:
57
+ return subprocess.CompletedProcess(
58
+ [sys.executable, str(USAGE_PROBE_PATH)],
59
+ returncode,
60
+ json.dumps(payload),
61
+ "",
62
+ )
63
+
64
+
65
+ def _event_stream(
66
+ session_id: str = "thread-1",
67
+ guidance: str = "PLAN\ninspect the change",
68
+ ) -> str:
69
+ return "\n".join(
70
+ [
71
+ json.dumps({"type": "thread.started", "thread_id": session_id}),
72
+ json.dumps(
73
+ {
74
+ "type": "item.completed",
75
+ "item": {
76
+ "type": "agent_message",
77
+ "text": guidance,
78
+ },
79
+ }
80
+ ),
81
+ ]
82
+ )
83
+
84
+
85
+ def _two_step_process_runner(calls: list[list[str]], guidance: str) -> _ProcessRunner:
86
+ def process_runner(
87
+ arguments: list[str], **kwargs: object
88
+ ) -> subprocess.CompletedProcess[str]:
89
+ calls.append(arguments)
90
+ if len(calls) == 1:
91
+ return _probe_process({"percent_left": 90})
92
+ return subprocess.CompletedProcess(
93
+ arguments, 0, _event_stream(guidance=guidance), ""
94
+ )
95
+
96
+ return process_runner
97
+
98
+
99
+ def test_sol_flag_accepts_documented_truthy_values() -> None:
100
+ assert sol_advisor.is_sol_advisor_enabled({"ADVISOR_SOL_XHIGH": "yes"})
101
+ assert not sol_advisor.is_sol_advisor_enabled({"ADVISOR_SOL_XHIGH": "0"})
102
+
103
+
104
+ def test_resolve_usage_probe_path_uses_supplied_home_directory(tmp_path: Path) -> None:
105
+ probe_path = sol_advisor.resolve_usage_probe_path(tmp_path)
106
+
107
+ assert probe_path == (
108
+ tmp_path
109
+ / ".claude"
110
+ / "skills"
111
+ / "codex-review"
112
+ / "scripts"
113
+ / "codex_usage_probe.py"
114
+ )
115
+
116
+
117
+ def test_argument_parser_accepts_bind_and_resume_modes() -> None:
118
+ parser = sol_advisor.build_argument_parser()
119
+
120
+ bind_arguments = parser.parse_args(["--bind", "--cwd", "."])
121
+ resume_arguments = parser.parse_args(
122
+ ["--resume", "thread-1", "--cwd", "."]
123
+ )
124
+
125
+ assert bind_arguments.bind
126
+ assert bind_arguments.resume is None
127
+ assert resume_arguments.resume == "thread-1"
128
+
129
+
130
+ def test_main_serializes_stable_result_field(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
131
+ monkeypatch.setattr(
132
+ sol_advisor,
133
+ "run_codex_sol_advisor",
134
+ lambda **kwargs: sol_advisor.CodexSolAdvisorReply(
135
+ session_id="thread-1",
136
+ guidance="PLAN\ninspect",
137
+ successful=True,
138
+ reason=None,
139
+ is_fallback=False,
140
+ signal="PLAN",
141
+ sol_enabled=True,
142
+ selected_tier=sol_advisor.ADVISOR_MODEL_TIER,
143
+ outcome=sol_advisor.CODEX_BIND_SUCCESS_TOKEN,
144
+ fallback_kind=None,
145
+ ),
146
+ )
147
+ monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
148
+
149
+ exit_code = sol_advisor.main(["--bind", "--cwd", "."])
150
+ payload = json.loads(capsys.readouterr().out)
151
+
152
+ assert exit_code == 0
153
+ assert payload["result"] == "codex"
154
+ assert "outcome" not in payload
155
+ assert payload["sol_enabled"] is True
156
+
157
+
158
+ def test_bind_and_resume_arguments_match_installed_codex_interface() -> None:
159
+ expected_common_arguments = [
160
+ "codex",
161
+ "exec",
162
+ "--model",
163
+ sol_advisor.ADVISOR_CODEX_MODEL_ID,
164
+ "--config",
165
+ 'model_reasoning_effort="xhigh"',
166
+ "--sandbox",
167
+ "read-only",
168
+ "--json",
169
+ ]
170
+ assert sol_advisor.build_codex_arguments("codex") == [
171
+ *expected_common_arguments,
172
+ "-",
173
+ ]
174
+ assert sol_advisor.build_codex_arguments(
175
+ WINDOWS_SHIM_PATH, session_id="thread-1"
176
+ ) == [
177
+ WINDOWS_SHIM_PATH,
178
+ *expected_common_arguments[1:],
179
+ "resume",
180
+ "thread-1",
181
+ "-",
182
+ ]
183
+
184
+
185
+ def test_resolve_codex_executable_prefers_the_env_var_override(
186
+ monkeypatch: pytest.MonkeyPatch,
187
+ ) -> None:
188
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
189
+
190
+ resolved_executable = sol_advisor.resolve_codex_executable(
191
+ {sol_advisor.ADVISOR_CODEX_EXECUTABLE_ENV_VAR: WINDOWS_SHIM_PATH}
192
+ )
193
+
194
+ assert resolved_executable == WINDOWS_SHIM_PATH
195
+
196
+
197
+ def test_resolve_codex_executable_falls_back_to_which_search(
198
+ monkeypatch: pytest.MonkeyPatch,
199
+ ) -> None:
200
+ monkeypatch.setattr(
201
+ sol_advisor.shutil,
202
+ "which",
203
+ lambda name: (
204
+ WINDOWS_SHIM_PATH if name == sol_advisor.CODEX_EXECUTABLE else None
205
+ ),
206
+ )
207
+
208
+ resolved_executable = sol_advisor.resolve_codex_executable({})
209
+
210
+ assert resolved_executable == WINDOWS_SHIM_PATH
211
+
212
+
213
+ def test_resolve_codex_executable_returns_none_when_unresolved(
214
+ monkeypatch: pytest.MonkeyPatch,
215
+ ) -> None:
216
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
217
+
218
+ resolved_executable = sol_advisor.resolve_codex_executable({})
219
+
220
+ assert resolved_executable is None
221
+
222
+
223
+ def test_bind_falls_back_with_a_clear_reason_when_executable_is_missing(
224
+ monkeypatch: pytest.MonkeyPatch,
225
+ ) -> None:
226
+ monkeypatch.setattr(sol_advisor.shutil, "which", lambda name: None)
227
+ calls: list[list[str]] = []
228
+
229
+ def process_runner(
230
+ arguments: list[str], **kwargs: object
231
+ ) -> subprocess.CompletedProcess[str]:
232
+ calls.append(arguments)
233
+ return _probe_process({"percent_left": 90})
234
+
235
+ reply = sol_advisor.run_codex_sol_advisor(
236
+ prompt="first consult",
237
+ working_directory=Path("."),
238
+ preflight=None,
239
+ probe_path=USAGE_PROBE_PATH,
240
+ session_id=None,
241
+ process_runner=process_runner,
242
+ setting_by_name={sol_advisor.SOL_ENV_VAR: "1"},
243
+ )
244
+
245
+ assert not reply.successful
246
+ assert reply.is_fallback
247
+ assert reply.reason is not None
248
+ assert "codex" in reply.reason
249
+ assert reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_BROKEN
250
+ assert calls == []
251
+
252
+
253
+ def test_policy_fallbacks_are_marked_declined() -> None:
254
+ def gate_closed_runner(
255
+ arguments: list[str], **kwargs: object
256
+ ) -> subprocess.CompletedProcess[str]:
257
+ return _probe_process({"percent_left": 5})
258
+
259
+ disabled_reply = sol_advisor.run_codex_sol_advisor(
260
+ prompt="first consult",
261
+ working_directory=Path("."),
262
+ preflight=None,
263
+ probe_path=USAGE_PROBE_PATH,
264
+ session_id=None,
265
+ process_runner=gate_closed_runner,
266
+ setting_by_name={},
267
+ )
268
+ gate_closed_reply = sol_advisor.run_codex_sol_advisor(
269
+ prompt="first consult",
270
+ working_directory=Path("."),
271
+ preflight=None,
272
+ probe_path=USAGE_PROBE_PATH,
273
+ session_id=None,
274
+ process_runner=gate_closed_runner,
275
+ setting_by_name=ENABLED_SETTING_BY_NAME,
276
+ )
277
+
278
+ assert disabled_reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_DECLINED
279
+ assert gate_closed_reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_DECLINED
280
+
281
+
282
+ def test_probe_failure_fallback_is_marked_broken() -> None:
283
+ def failing_probe_runner(
284
+ arguments: list[str], **kwargs: object
285
+ ) -> subprocess.CompletedProcess[str]:
286
+ return _probe_process({}, returncode=3)
287
+
288
+ reply = sol_advisor.run_codex_sol_advisor(
289
+ prompt="first consult",
290
+ working_directory=Path("."),
291
+ preflight=None,
292
+ probe_path=USAGE_PROBE_PATH,
293
+ session_id=None,
294
+ process_runner=failing_probe_runner,
295
+ setting_by_name=ENABLED_SETTING_BY_NAME,
296
+ )
297
+
298
+ assert reply.is_fallback
299
+ assert reply.fallback_kind == sol_advisor.SOL_FALLBACK_KIND_BROKEN
300
+
301
+
302
+ def test_enable_sol_flag_opens_the_rung_without_an_environment_flag(
303
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
304
+ ) -> None:
305
+ captured_settings: dict[str, str] = {}
306
+
307
+ def fake_advisor(**kwargs: object) -> object:
308
+ captured_settings.update(dict(kwargs["setting_by_name"])) # type: ignore[arg-type]
309
+ return sol_advisor._reply_fallback(
310
+ "probe declined",
311
+ True,
312
+ fallback_kind=sol_advisor.SOL_FALLBACK_KIND_DECLINED,
313
+ )
314
+
315
+ monkeypatch.setattr(sol_advisor, "run_codex_sol_advisor", fake_advisor)
316
+ monkeypatch.delenv(sol_advisor.SOL_ENV_VAR, raising=False)
317
+ monkeypatch.setattr(sys, "stdin", io.StringIO("first consult"))
318
+
319
+ exit_code = sol_advisor.main(
320
+ ["--bind", "--cwd", ".", sol_advisor.SOL_ENABLE_FLAG]
321
+ )
322
+ payload = json.loads(capsys.readouterr().out)
323
+
324
+ assert exit_code == 1
325
+ assert captured_settings[sol_advisor.SOL_ENV_VAR] == "1"
326
+ assert payload["fallback_kind"] == sol_advisor.SOL_FALLBACK_KIND_DECLINED
327
+
328
+
329
+ def test_successful_probe_requires_finite_meter_above_configured_gate() -> None:
330
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
331
+ return _probe_process({"percent_left": 90})
332
+
333
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
334
+
335
+ assert preflight.eligible
336
+ assert preflight.percent_left == 90
337
+
338
+
339
+ @pytest.mark.parametrize("percent_left", [10, 10.0, 10.000])
340
+ def test_meter_at_configured_threshold_falls_back(
341
+ percent_left: float,
342
+ ) -> None:
343
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
344
+ return _probe_process({"percent_left": percent_left})
345
+
346
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
347
+
348
+ assert not preflight.eligible
349
+ assert preflight.percent_left == percent_left
350
+
351
+
352
+ @pytest.mark.parametrize(
353
+ "payload",
354
+ [
355
+ {"percent_left": None},
356
+ {"percent_left": "90"},
357
+ {"percent_left": True},
358
+ {"percent_left": float("nan")},
359
+ ["percent_left", 90],
360
+ ],
361
+ )
362
+ def test_unknown_and_malformed_meter_falls_back(payload: object) -> None:
363
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
364
+ return _probe_process(payload)
365
+
366
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
367
+
368
+ assert not preflight.eligible
369
+ assert preflight.percent_left is None
370
+
371
+
372
+ def test_malformed_probe_json_falls_back() -> None:
373
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
374
+ return subprocess.CompletedProcess(arguments, 0, "{broken", "")
375
+
376
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
377
+
378
+ assert not preflight.eligible
379
+
380
+
381
+ def test_probe_nonzero_exit_falls_back() -> None:
382
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
383
+ return _probe_process({"percent_left": 90}, returncode=2)
384
+
385
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
386
+
387
+ assert not preflight.eligible
388
+
389
+
390
+ def test_probe_timeout_falls_back() -> None:
391
+ def probe_runner(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
392
+ raise subprocess.TimeoutExpired(arguments, 30)
393
+
394
+ preflight = sol_advisor.run_sol_preflight(USAGE_PROBE_PATH, probe_runner)
395
+
396
+ assert not preflight.eligible
397
+ assert "timed out" in preflight.reason
398
+
399
+
400
+ @pytest.mark.parametrize("signal", ["ENDORSE", "CORRECTION", "PLAN", "STOP"])
401
+ def test_jsonl_parser_accepts_each_exact_guidance_signal(signal: str) -> None:
402
+ reply = sol_advisor.parse_codex_jsonl_reply(
403
+ _event_stream(guidance=f"{signal}\nadditional guidance"),
404
+ existing_session_id=None,
405
+ is_sol_enabled=True,
406
+ )
407
+
408
+ assert reply.successful
409
+ assert not reply.is_fallback
410
+ assert reply.session_id == "thread-1"
411
+ assert reply.guidance == f"{signal}\nadditional guidance"
412
+
413
+
414
+ @pytest.mark.parametrize(
415
+ "jsonl_text",
416
+ [
417
+ "{broken",
418
+ json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
419
+ _event_stream(session_id="", guidance="PLAN\ninspect"),
420
+ _event_stream(guidance="ENDORSE: ready"),
421
+ _event_stream(guidance="\n unknown signal\nmore"),
422
+ json.dumps(
423
+ {
424
+ "type": "item.completed",
425
+ "item": {"type": "agent_message", "text": "PLAN"},
426
+ }
427
+ ),
428
+ ],
429
+ )
430
+ def test_jsonl_parser_returns_typed_fallback_for_invalid_reply(
431
+ jsonl_text: str,
432
+ ) -> None:
433
+ reply = sol_advisor.parse_codex_jsonl_reply(
434
+ jsonl_text,
435
+ existing_session_id=None,
436
+ is_sol_enabled=True,
437
+ )
438
+
439
+ assert not reply.successful
440
+ assert reply.is_fallback
441
+ assert reply.session_id is None
442
+ assert reply.guidance is None
443
+ assert reply.reason
444
+
445
+
446
+ def test_bind_runs_probe_then_codex_with_read_only_xhigh_settings() -> None:
447
+ calls: list[tuple[list[str], dict[str, object]]] = []
448
+
449
+ def process_runner(
450
+ arguments: list[str], **kwargs: object
451
+ ) -> subprocess.CompletedProcess[str]:
452
+ calls.append((arguments, kwargs))
453
+ if len(calls) == 1:
454
+ return _probe_process({"percent_left": 90})
455
+ return subprocess.CompletedProcess(
456
+ arguments, 0, _event_stream(guidance="PLAN\ninspect"), ""
457
+ )
458
+
459
+ reply = sol_advisor.run_codex_sol_advisor(
460
+ prompt="reply only",
461
+ working_directory=Path("."),
462
+ preflight=None,
463
+ probe_path=USAGE_PROBE_PATH,
464
+ session_id=None,
465
+ process_runner=process_runner,
466
+ setting_by_name=ENABLED_SETTING_BY_NAME,
467
+ )
468
+
469
+ assert reply.successful
470
+ assert len(calls) == 2
471
+ assert calls[1][0] == sol_advisor.build_codex_arguments("codex")
472
+ assert calls[1][1]["cwd"] == "."
473
+ assert calls[1][1]["shell"] is False
474
+ assert calls[1][1]["timeout"]
475
+
476
+
477
+ def test_team_advisor_path_preserves_sol_routing_fields() -> None:
478
+ team_advisor_path = SCRIPTS_ROOT.parents[2] / "skills" / "team-advisor" / "SKILL.md"
479
+ sol_rung_path = SCRIPTS_ROOT.parent / "reference" / "sol-rung.md"
480
+ assert "advisor-protocol.md" in team_advisor_path.read_text(encoding="utf-8")
481
+ sol_rung = sol_rung_path.read_text(encoding="utf-8")
482
+ assert "codex_sol_advisor.py" in sol_rung
483
+ assert "--resume <session_id>" in sol_rung
484
+ calls: list[list[str]] = []
485
+
486
+ reply = sol_advisor.run_codex_sol_advisor(
487
+ prompt="first consult",
488
+ working_directory=Path("."),
489
+ preflight=None,
490
+ probe_path=USAGE_PROBE_PATH,
491
+ session_id=None,
492
+ process_runner=_two_step_process_runner(calls, guidance="PLAN\ninspect"),
493
+ setting_by_name=ENABLED_SETTING_BY_NAME,
494
+ )
495
+
496
+ assert reply.successful
497
+ assert reply.sol_enabled
498
+ assert reply.selected_tier == sol_advisor.ADVISOR_MODEL_TIER
499
+ assert reply.outcome == sol_advisor.CODEX_BIND_SUCCESS_TOKEN
500
+ assert reply.signal == "PLAN"
501
+ assert reply.guidance == "PLAN\ninspect"
502
+
503
+
504
+ def test_team_advisor_path_uses_fable_result_when_sol_gate_is_closed() -> None:
505
+ calls: list[list[str]] = []
506
+
507
+ def process_runner(
508
+ arguments: list[str], **kwargs: object
509
+ ) -> subprocess.CompletedProcess[str]:
510
+ calls.append(arguments)
511
+ return _probe_process({"percent_left": 10})
512
+
513
+ reply = sol_advisor.run_codex_sol_advisor(
514
+ prompt="first consult",
515
+ working_directory=Path("."),
516
+ preflight=None,
517
+ probe_path=USAGE_PROBE_PATH,
518
+ session_id=None,
519
+ process_runner=process_runner,
520
+ setting_by_name=ENABLED_SETTING_BY_NAME,
521
+ )
522
+
523
+ assert not reply.successful
524
+ assert reply.is_fallback
525
+ assert reply.sol_enabled
526
+ assert reply.selected_tier == sol_advisor.ADVISOR_FALLBACK_TIER
527
+ assert reply.outcome == sol_advisor.ADVISOR_FALLBACK_RESULT
528
+ assert len(calls) == 1
529
+
530
+
531
+ def test_disabled_flag_uses_default_optional_routing_inputs(
532
+ monkeypatch: pytest.MonkeyPatch,
533
+ ) -> None:
534
+ monkeypatch.delenv(sol_advisor.SOL_ENV_VAR, raising=False)
535
+ reply = sol_advisor.run_codex_sol_advisor(
536
+ prompt="first consult",
537
+ working_directory=Path("."),
538
+ preflight=sol_advisor.SolPreflight(
539
+ eligible=True,
540
+ percent_left=90,
541
+ reason="test preflight",
542
+ ),
543
+ probe_path=None,
544
+ setting_by_name=None,
545
+ session_id=None,
546
+ process_runner=subprocess.run,
547
+ )
548
+
549
+ assert reply.is_fallback
550
+ assert reply.reason == "Sol advisor flag is disabled"
551
+
552
+
553
+ def test_resume_runs_the_usage_gate_before_codex() -> None:
554
+ calls: list[list[str]] = []
555
+
556
+ reply = sol_advisor.run_codex_sol_advisor(
557
+ prompt="resume",
558
+ working_directory=Path("."),
559
+ preflight=None,
560
+ session_id="thread-1",
561
+ probe_path=USAGE_PROBE_PATH,
562
+ process_runner=_two_step_process_runner(calls, guidance="ENDORSE\nready"),
563
+ setting_by_name=ENABLED_SETTING_BY_NAME,
564
+ )
565
+
566
+ assert reply.successful
567
+ assert calls == [
568
+ [sys.executable, str(USAGE_PROBE_PATH)],
569
+ sol_advisor.build_codex_arguments("codex", session_id="thread-1"),
570
+ ]
571
+
572
+
573
+ @pytest.mark.parametrize(
574
+ "codex_failure",
575
+ [
576
+ "nonzero",
577
+ "timeout",
578
+ "malformed_jsonl",
579
+ "missing_session",
580
+ "missing_guidance",
581
+ "invalid_signal",
582
+ ],
583
+ )
584
+ def test_codex_failure_modes_always_return_fallback(
585
+ codex_failure: str,
586
+ ) -> None:
587
+ def process_runner(
588
+ arguments: list[str], **kwargs: object
589
+ ) -> subprocess.CompletedProcess[str]:
590
+ if len(arguments) == 2:
591
+ return _probe_process({"percent_left": 90})
592
+ if codex_failure == "timeout":
593
+ raise subprocess.TimeoutExpired(arguments, 30)
594
+ if codex_failure == "nonzero":
595
+ return subprocess.CompletedProcess(
596
+ arguments, 3, _event_stream(guidance="PLAN"), ""
597
+ )
598
+ if codex_failure == "malformed_jsonl":
599
+ return subprocess.CompletedProcess(arguments, 0, "{broken", "")
600
+ if codex_failure == "missing_session":
601
+ return subprocess.CompletedProcess(
602
+ arguments,
603
+ 0,
604
+ json.dumps(
605
+ {
606
+ "type": "item.completed",
607
+ "item": {"type": "agent_message", "text": "PLAN"},
608
+ }
609
+ ),
610
+ "",
611
+ )
612
+ if codex_failure == "missing_guidance":
613
+ return subprocess.CompletedProcess(
614
+ arguments,
615
+ 0,
616
+ json.dumps({"type": "thread.started", "thread_id": "thread-1"}),
617
+ "",
618
+ )
619
+ return subprocess.CompletedProcess(
620
+ arguments, 0, _event_stream(guidance="PLAN: inspect"), ""
621
+ )
622
+
623
+ reply = sol_advisor.run_codex_sol_advisor(
624
+ prompt="bind",
625
+ working_directory=Path("."),
626
+ preflight=None,
627
+ probe_path=USAGE_PROBE_PATH,
628
+ session_id=None,
629
+ process_runner=process_runner,
630
+ setting_by_name=ENABLED_SETTING_BY_NAME,
631
+ )
632
+
633
+ assert not reply.successful
634
+ assert reply.is_fallback
635
+ assert reply.session_id is None
636
+ assert reply.guidance is None