diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index a8375544da..99fda4da4a 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -61,10 +61,17 @@ async def exec( cwd=cwd, ) payload = _maybe_model_dump(result) + data = payload.get("data") + if isinstance(data, dict): + payload.update( + {key: value for key, value in data.items() if value is not None} + ) - stdout = payload.get("output", payload.get("stdout", "")) or "" - stderr = payload.get("error", payload.get("stderr", "")) or "" + stdout = payload.get("output") or payload.get("stdout") or "" + stderr = payload.get("error") or payload.get("stderr") or "" exit_code = payload.get("exit_code") + if exit_code is None: + exit_code = payload.get("return_code") if background: pid: int | None = None try: diff --git a/tests/unit/test_computer.py b/tests/unit/test_computer.py index e667f98a6c..940f8fbfa7 100644 --- a/tests/unit/test_computer.py +++ b/tests/unit/test_computer.py @@ -376,6 +376,74 @@ def test_base_class_is_protocol(self): class TestShipyardBooter: """Tests for ShipyardBooter.""" + @pytest.mark.asyncio + async def test_shipyard_shell_unwraps_bay_exec_response(self): + """Bay exec responses keep shell output under the data field.""" + from astrbot.core.computer.booters.shipyard import ShipyardShellWrapper + + raw_shell = AsyncMock() + raw_shell.exec = AsyncMock( + return_value={ + "success": True, + "data": { + "success": True, + "return_code": 0, + "stdout": "hello_from_shipyard_bay\n/workspace\n", + "stderr": "", + "pid": 16, + "process_id": None, + "error": None, + }, + "error": None, + } + ) + + result = await ShipyardShellWrapper(raw_shell).exec( + "echo hello_from_shipyard_bay && pwd" + ) + + assert result == { + "stdout": "hello_from_shipyard_bay\n/workspace\n", + "stderr": "", + "exit_code": 0, + "success": True, + "execution_id": None, + "execution_time_ms": None, + "command": None, + } + + @pytest.mark.asyncio + async def test_shipyard_shell_keeps_flat_exec_response_compatible(self): + """Flat shell responses remain compatible with ShipyardShellWrapper.""" + from astrbot.core.computer.booters.shipyard import ShipyardShellWrapper + + raw_shell = AsyncMock() + raw_shell.exec = AsyncMock( + return_value={ + "success": True, + "stdout": "hello_from_legacy_shell\n/workspace\n", + "stderr": "", + "exit_code": 0, + "execution_id": "exec-legacy", + "execution_time_ms": 45, + "command": "echo hello_from_legacy_shell && pwd", + } + ) + + result = await ShipyardShellWrapper(raw_shell).exec( + "echo hello_from_legacy_shell && pwd" + ) + + assert result == { + "stdout": "hello_from_legacy_shell\n/workspace\n", + "stderr": "", + "exit_code": 0, + "success": True, + "execution_id": "exec-legacy", + "execution_time_ms": 45, + "command": "echo hello_from_legacy_shell && pwd", + } + @pytest.mark.asyncio async def test_shipyard_booter_init(self): """Test ShipyardBooter initialization."""