Files
LangBot/plugins/Runner/LocalAgent/tests/test_box_recovery.py
T

118 lines
4.6 KiB
Python

"""Export errors are ordinary tool results; no completion recovery hook."""
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot_plugin.api.entities.builtin.runner.box import BoxFile
from pkg.agent_core.langbot import LangBotToolExecutor
from pkg.agent_core.types import ToolCallRequest
from pkg.box import LocalAgentBox
@pytest.mark.asyncio
async def test_explicit_export_error_then_retry():
ctx = SimpleNamespace(
export_box_files=AsyncMock(
side_effect=[RuntimeError("file missing: /out/a"), [BoxFile(id="file", name="a", type="File", size=1)]]
),
delivery=SimpleNamespace(automatic_reply=True),
reply_files=AsyncMock(),
)
box = LocalAgentBox(ctx, None)
box.prepare = AsyncMock()
executor = LangBotToolExecutor(None, set())
assembly = SimpleNamespace(tools=[], tool_executor=executor)
box.install_tool(assembly)
request = ToolCallRequest.from_raw(
{
"id": "export",
"type": "function",
"function": {"name": "export_box_files", "arguments": '{"paths": ["/out/a"]}'},
}
)
failed = await executor.execute(executor.prepare(request))
assert "file missing" in failed.error
assert not box.file_ids
success = await executor.execute(executor.prepare(request))
assert success.error is None
assert box.file_ids == ["file"]
assert not hasattr(box, "before_complete")
assert not hasattr(box, "finish")
@pytest.mark.asyncio
@pytest.mark.parametrize("automatic", [False, True])
async def test_parallel_exports_keep_handles_and_delivery_unique(automatic):
import asyncio
file = BoxFile(id="file", name="a", type="File", size=1, warning="cleanup failed")
ctx = SimpleNamespace(
export_box_files=AsyncMock(return_value=[file]),
delivery=SimpleNamespace(automatic_reply=automatic),
reply_files=AsyncMock(),
)
box = LocalAgentBox(ctx, None)
box.prepare = AsyncMock()
results = await asyncio.gather(box.export_files(["/out/a"]), box.export_files(["/out/a"]))
assert all(r["status"] == "accepted" and r["items"][0]["warning"] == "cleanup failed" for r in results)
assert box.file_ids == (["file"] if automatic else [])
assert ctx.reply_files.await_count == (0 if automatic else 1)
assert not hasattr(box, "before_complete") and not hasattr(box, "finish")
@pytest.mark.asyncio
@pytest.mark.parametrize("failure", [RuntimeError("send failed"), PermissionError("no permission")])
async def test_platform_failure_is_fatal_and_cannot_reexport_or_redeliver(failure):
from pkg.agent_core.types import NonRetryableToolError
ctx = SimpleNamespace(
export_box_files=AsyncMock(return_value=[BoxFile(id="file", name="a", type="File", size=1)]),
delivery=SimpleNamespace(automatic_reply=False),
reply_files=AsyncMock(side_effect=failure),
)
box = LocalAgentBox(ctx, None)
box.prepare = AsyncMock()
executor = LangBotToolExecutor(None, set())
box.install_tool(SimpleNamespace(tools=[], tool_executor=executor))
request = ToolCallRequest.from_raw(
{"id": "call", "function": {"name": "export_box_files", "arguments": '{"paths":["/out/a"]}'}}
)
for _ in range(2):
with pytest.raises(NonRetryableToolError, match="platform delivery failed"):
await executor.execute(executor.prepare(request))
ctx.export_box_files.assert_awaited_once_with(["/out/a"])
ctx.reply_files.assert_awaited_once_with(["file"])
@pytest.mark.asyncio
async def test_invalid_host_response_is_not_a_generation_retry():
from pkg.agent_core.types import NonRetryableToolError
ctx = SimpleNamespace(
export_box_files=AsyncMock(
side_effect=RuntimeError(
'ActionCallError: LANGBOT_EXPORT_V1:{"code":"box.export.invalid_response","message":"invalid attachment data","retryable":false}'
)
)
)
box = LocalAgentBox(ctx, None)
box.prepare = AsyncMock()
with pytest.raises(NonRetryableToolError, match="invalid attachment data"):
await box.export_files(["/out/a"])
@pytest.mark.asyncio
async def test_legacy_sdk_fails_before_acquiring_a_box(monkeypatch):
from langbot_plugin.api.entities.builtin.runner.context import RunnerContext
async def old_export(self):
raise AssertionError("must not be called")
monkeypatch.setattr(RunnerContext, "export_box_files", old_export)
api = SimpleNamespace(get_box_status=AsyncMock())
with pytest.raises(RuntimeError, match="upgrade them together"):
await LocalAgentBox(SimpleNamespace(), api).prepare()
api.get_box_status.assert_not_called()