From ae1181dc148cff3eaf499bb58b3ae4b121367921 Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Wed, 11 Nov 2026 18:46:34 +0000 Subject: [PATCH 1/2] fix(computers): a failed resolve reports apt's error, not the resolver's traceback --- .../env/envs/computer/utils/resolve_in_base.py | 11 ++++++++++- tst/unit/env/envs/test_computer_env.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/agent_env/env/envs/computer/utils/resolve_in_base.py b/src/agent_env/env/envs/computer/utils/resolve_in_base.py index 6425a4955..3b02f9704 100644 --- a/src/agent_env/env/envs/computer/utils/resolve_in_base.py +++ b/src/agent_env/env/envs/computer/utils/resolve_in_base.py @@ -69,5 +69,14 @@ def _wheels(layer: dict, dest: Path) -> list[str]: return sorted(path.name for path in dest.glob("*.whl")) +def cli(argv: list[str]) -> None: + """``main`` on ``argv``. A command that fails ends it with that command's exit code and no traceback, so the last + line of the output, which the composer reports as the reason, is the command's own error (apt's ``E:`` line).""" + try: + main(*argv) + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + + if __name__ == "__main__": - main(*sys.argv[1:]) + cli(sys.argv[1:]) diff --git a/tst/unit/env/envs/test_computer_env.py b/tst/unit/env/envs/test_computer_env.py index 3679af1f1..d3f98945a 100644 --- a/tst/unit/env/envs/test_computer_env.py +++ b/tst/unit/env/envs/test_computer_env.py @@ -13,6 +13,7 @@ import logging import re import shutil +import subprocess import urllib.parse from datetime import timedelta from pathlib import Path @@ -2046,6 +2047,21 @@ def _local(work, context): return builder.LocalBuilder("LocalSandboxProvider", work, base, "linux/arm64") +def test_a_command_the_resolver_runs_that_fails_ends_it_with_the_commands_code_and_no_traceback(monkeypatch, capsys): + """The composer reports the output's last line as the reason: it must be the command's error, apt's ``E:`` line, + not a traceback naming the command.""" + def fail(*_args): + raise subprocess.CalledProcessError(100, ["apt-get", "install"]) + + monkeypatch.setattr(resolve_in_base, "main", fail) + + with pytest.raises(SystemExit) as exited: + resolve_in_base.cli(["request.json", "out"]) + + assert exited.value.code == 100 + assert capsys.readouterr().err == "" + + def test_the_resolver_installs_the_apt_packages_it_downloads_before_building_wheels(tmp_path, monkeypatch): ran = [] monkeypatch.setattr(resolve_in_base, "KEYRINGS", tmp_path / "keyrings") From 417b12f7d0a4d54da0fcd2fd4ffcc8233ad2c52e Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Wed, 11 Nov 2026 19:07:10 +0000 Subject: [PATCH 2/2] fix(computers): a failed command whose output was captured has it printed before the resolver exits --- .../env/envs/computer/utils/resolve_in_base.py | 6 +++++- tst/unit/env/envs/test_computer_env.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/agent_env/env/envs/computer/utils/resolve_in_base.py b/src/agent_env/env/envs/computer/utils/resolve_in_base.py index 3b02f9704..88f153957 100644 --- a/src/agent_env/env/envs/computer/utils/resolve_in_base.py +++ b/src/agent_env/env/envs/computer/utils/resolve_in_base.py @@ -71,10 +71,14 @@ def _wheels(layer: dict, dest: Path) -> list[str]: def cli(argv: list[str]) -> None: """``main`` on ``argv``. A command that fails ends it with that command's exit code and no traceback, so the last - line of the output, which the composer reports as the reason, is the command's own error (apt's ``E:`` line).""" + line of the output, which the composer reports as the reason, is the command's own error (apt's ``E:`` line); one + whose output was captured has it printed first.""" try: main(*argv) except subprocess.CalledProcessError as e: + captured = e.stderr or e.stdout + if captured: + sys.stderr.write(captured if isinstance(captured, str) else captured.decode(errors="replace")) sys.exit(e.returncode) diff --git a/tst/unit/env/envs/test_computer_env.py b/tst/unit/env/envs/test_computer_env.py index d3f98945a..704360b3b 100644 --- a/tst/unit/env/envs/test_computer_env.py +++ b/tst/unit/env/envs/test_computer_env.py @@ -2062,6 +2062,19 @@ def fail(*_args): assert capsys.readouterr().err == "" +def test_a_captured_command_that_fails_has_its_error_printed_last(monkeypatch, capsys): + def fail(*_args): + raise subprocess.CalledProcessError(2, ["dpkg-deb", "-f"], stderr="dpkg-deb: error: not a Debian archive\n") + + monkeypatch.setattr(resolve_in_base, "main", fail) + + with pytest.raises(SystemExit) as exited: + resolve_in_base.cli(["request.json", "out"]) + + assert exited.value.code == 2 + assert capsys.readouterr().err.strip().splitlines()[-1] == "dpkg-deb: error: not a Debian archive" + + def test_the_resolver_installs_the_apt_packages_it_downloads_before_building_wheels(tmp_path, monkeypatch): ran = [] monkeypatch.setattr(resolve_in_base, "KEYRINGS", tmp_path / "keyrings")