Files
microsoft--skillopt/tests/test_claude_code_exec_resolution.py
Nuplum 2e23a25ff9 Fixes the Claude Code Exec backend for issue #233 (#238)
* feat(claude_code_exec): add claude code optimizer backend with SDK trace support

Register claude_code_exec as a full optimizer/target backend (issue #233).
--backend claude_code_exec now defaults both roles to claude_code_exec so
reflection sees the agent's complete session, and the SDK message stream is
parsed into structured trace steps persisted as claude_trace_steps.txt and
injected into the analyst prompt.

- model/claude_code_backend.py (new): chat_optimizer/chat_optimizer_messages on
  run_claude_code_chat, reasoning_effort threaded through, retry loop that
  surfaces non-JSON structured replies as RuntimeError, token tracking.
- model/codex_harness.py: parse/format/persist claude trace steps (text,
  tool_call, tool_result; drops init/thinking_tokens; 200-char tool_result cap;
  total truncation) + effort override on run_claude_code_chat.
- trainer.py/reflect.py: inject Claude Trace Steps gated behind
  REFLACT_CLAUDE_TRACE_TO_OPTIMIZER, set by the trainer only for claude_code_exec
  targets with model.claude_trace_to_optimizer (mirrors codex gate; default true).
- config.py/default.yaml/docs: model.claude_trace_to_optimizer key + flatten
  mapping + config.md rows.
- backend_config.py + model/__init__.py: register backend, route chat dispatch,
  token summary, reasoning effort, deployments.
- scripts/train.py, eval_only.py: symmetric default + accurate comments.
- tests: tests/test_claude_code_backend.py (10 tests: parsing, dispatch, effort,
  retry, trainer/reflect gating); test_role_backend_resolution.py updated to the
  symmetric default.

Verified: 58 unit tests pass; integration smoke on searchqa improved best-on-val
0.7500 -> 0.9375 with 80 claude_trace_steps.txt written; all output files valid
UTF-8 (no GBK mojibake).

* fix(claude_code_exec): address #233 review feedback
2026-08-23 15:02:43 +08:00

142 lines
4.3 KiB
Python

"""Config-resolution regressions for the claude_code_exec backend (issue #233).
Route B: ``--backend claude_code_exec`` defaults only the *target* to Claude
Code; the optimizer keeps its configured backend (openai_chat by default).
Opting the optimizer in via ``--optimizer_backend claude_code_exec`` must then
normalize its model to the Claude default rather than leaving ``gpt-5.5``.
"""
from __future__ import annotations
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
import scripts.train as train_script
_ROOT = Path(__file__).resolve().parents[1]
def _train_cfg(monkeypatch, *extra_argv: str) -> dict:
monkeypatch.setattr(
sys,
"argv",
[
"skillopt-train",
"--config",
str(_ROOT / "configs" / "searchqa" / "default.yaml"),
*extra_argv,
],
)
return train_script.load_config(train_script.parse_args())
def test_train_claude_code_exec_defaults_target_only(monkeypatch) -> None:
cfg = _train_cfg(monkeypatch, "--backend", "claude_code_exec")
assert cfg["optimizer_backend"] == "openai_chat"
assert cfg["optimizer_model"] == "gpt-5.5"
assert cfg["target_backend"] == "claude_code_exec"
assert cfg["target_model"] == "claude-sonnet-4-6"
def test_train_claude_code_exec_optimizer_opt_in_normalizes_model(monkeypatch) -> None:
cfg = _train_cfg(
monkeypatch,
"--backend",
"claude_code_exec",
"--optimizer_backend",
"claude_code_exec",
)
assert cfg["optimizer_backend"] == "claude_code_exec"
assert cfg["optimizer_model"] == "claude-sonnet-4-6"
assert cfg["target_backend"] == "claude_code_exec"
assert cfg["target_model"] == "claude-sonnet-4-6"
class _StopAfterResolution(Exception):
pass
def _run_eval_resolution(
monkeypatch, tmp_path, *, optimizer_backend: str | None
) -> dict:
import scripts.eval_only as eval_script
skill_path = tmp_path / "skill.md"
skill_path.write_text("# Test skill\n", encoding="utf-8")
cfg = {
"model": {
"backend": "azure_openai",
"optimizer": "gpt-5.5",
"target": "gpt-5.5",
"optimizer_backend": "openai_chat",
"target_backend": "openai_chat",
},
"env": {"out_root": str(tmp_path / "out")},
}
args = SimpleNamespace(
config="unused.yaml",
skill=str(skill_path),
split=None,
cfg_options=[],
backend="claude_code_exec",
optimizer_backend=optimizer_backend,
)
monkeypatch.setattr(eval_script, "parse_args", lambda: args)
monkeypatch.setattr("skillopt.config.load_config", lambda *a, **kw: cfg)
observed: dict[str, str] = {}
def capture(name):
def _fn(value, *a, **k):
observed[name] = value
return _fn
monkeypatch.setattr(eval_script, "configure_azure_openai", lambda **kw: None)
monkeypatch.setattr(eval_script, "set_optimizer_backend", capture("optimizer_backend"))
monkeypatch.setattr(eval_script, "set_target_backend", capture("target_backend"))
monkeypatch.setattr(eval_script, "set_optimizer_deployment", capture("optimizer_model"))
monkeypatch.setattr(eval_script, "set_target_deployment", capture("target_model"))
def stop(*a, **k):
raise _StopAfterResolution
monkeypatch.setattr(eval_script, "configure_codex_exec_from_config", stop)
with pytest.raises(_StopAfterResolution):
eval_script.main()
return observed
def test_eval_claude_code_exec_defaults_target_only(monkeypatch, tmp_path) -> None:
observed = _run_eval_resolution(monkeypatch, tmp_path, optimizer_backend=None)
assert observed == {
"optimizer_backend": "openai_chat",
"target_backend": "claude_code_exec",
"optimizer_model": "gpt-5.5",
"target_model": "claude-sonnet-4-6",
}
def test_eval_claude_code_exec_optimizer_opt_in_normalizes_model(
monkeypatch, tmp_path
) -> None:
observed = _run_eval_resolution(
monkeypatch, tmp_path, optimizer_backend="claude_code_exec"
)
assert observed == {
"optimizer_backend": "claude_code_exec",
"target_backend": "claude_code_exec",
"optimizer_model": "claude-sonnet-4-6",
"target_model": "claude-sonnet-4-6",
}