Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/inspect_robots/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
from inspect_robots.rollout import TrialRecord
from inspect_robots.scene import Scene
from inspect_robots.spaces import Box
from inspect_robots.task import Task


def _styled(text: str, code: str) -> str:
Expand Down Expand Up @@ -478,6 +479,23 @@ def _resolve_or_exit(
) from exc


def _apply_epochs_or_exit(task: Task, epochs: int) -> Task:
"""Apply ``--epochs`` with a guided error instead of a raw traceback (#145).

``replace()`` reruns ``Task.__post_init__``, which rejects a count below 1
via ``ConfigError`` — the same validation-error class ``_resolve_or_exit``
already converts to ``SystemExit`` for registry resolution.
"""
from dataclasses import replace

from inspect_robots.errors import ConfigError

try:
return replace(task, epochs=epochs)
except ConfigError as exc:
raise SystemExit(str(exc)) from exc


def _build_guardrails(
space: Box, max_action_delta: float | None
) -> tuple[Approver, list[str], list[str]]:
Expand Down Expand Up @@ -835,8 +853,6 @@ def _build_and_announce_guardrails(args: argparse.Namespace, action_space: Box)


def _cmd_run(args: argparse.Namespace) -> int:
from dataclasses import replace

from inspect_robots import eval
from inspect_robots.logging import JsonLogSink
from inspect_robots.scene import Scene
Expand Down Expand Up @@ -885,7 +901,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
embodiment = resolved.embodiment
try:
if args.epochs is not None:
task = replace(task, epochs=args.epochs)
task = _apply_epochs_or_exit(task, args.epochs)

_announce_components(resolved)
approver = _build_and_announce_guardrails(args, embodiment.info.action_space)
Expand Down Expand Up @@ -995,8 +1011,6 @@ def _cmd_eval_set(args: argparse.Namespace) -> int:
the embodiment once per task), the CLI resolves the embodiment exactly
once for the whole set, so a real robot is not reconnected between tasks.
"""
from dataclasses import replace

from inspect_robots import eval_set

_check_shared_run_conflicts(args)
Expand All @@ -1005,7 +1019,7 @@ def _cmd_eval_set(args: argparse.Namespace) -> int:
defaults = load_defaults(os.environ)
tasks = [_resolve_or_exit("task", name) for name in task_names]
if args.epochs is not None:
tasks = [replace(t, epochs=args.epochs) for t in tasks]
tasks = [_apply_epochs_or_exit(t, args.epochs) for t in tasks]

resolved = _resolve_components(args, defaults)
embodiment = resolved.embodiment
Expand Down
32 changes: 27 additions & 5 deletions tests/test_registry_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,29 @@ def test_cli_eval_set_sim_and_embodiment_conflict() -> None:
main(["eval-set", "cubepick-reach", "--sim", "--embodiment", "cubepick"])


def test_cli_eval_set_negative_epochs_is_a_guided_error(tmp_path: Path) -> None:
"""--epochs -1 must not leak Task.__post_init__'s raw ConfigError (#145)."""
_register_task("kb/a")
try:
with pytest.raises(SystemExit, match="Epochs count must be >= 1, got -1"):
main(
[
"eval-set",
"kb/a",
"--policy",
"scripted",
"--embodiment",
"cubepick",
"--epochs",
"-1",
"--log-dir",
str(tmp_path),
]
)
finally:
reg._FACTORIES["task"].pop("kb/a", None)


def test_cli_eval_set_guardrail_flags_conflict() -> None:
with pytest.raises(SystemExit, match="drop one"):
main(
Expand Down Expand Up @@ -2462,10 +2485,9 @@ def close(self) -> None:
def test_cli_run_closes_embodiment_when_validation_raises(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""A failure between resolving the embodiment and eval() (here: --epochs 0
raising ConfigError) must still close the embodiment — otherwise a bad flag
leaves real arms energized."""
from inspect_robots.errors import ConfigError
"""A failure between resolving the embodiment and eval() (here: --epochs 0,
a guided SystemExit per #145) must still close the embodiment — otherwise a
bad flag leaves real arms energized."""
from inspect_robots.mock import CubePickEmbodiment

closed: list[bool] = []
Expand All @@ -2478,7 +2500,7 @@ def close(self) -> None:
monkeypatch.setitem(reg._FACTORIES["embodiment"], "tracked-cubepick", _Tracked)
monkeypatch.setenv(ENV_POLICY, "scripted")
monkeypatch.setenv(ENV_EMBODIMENT, "tracked-cubepick")
with pytest.raises(ConfigError):
with pytest.raises(SystemExit, match="Epochs count must be >= 1"):
main(
[
"reach the cube",
Expand Down
Loading