diff --git a/backend/tests/test_tool_output_budget_middleware.py b/backend/tests/test_tool_output_budget_middleware.py index 82e940adf3..da64761395 100644 --- a/backend/tests/test_tool_output_budget_middleware.py +++ b/backend/tests/test_tool_output_budget_middleware.py @@ -8,8 +8,10 @@ from __future__ import annotations +import contextlib import json import os +import pathlib import tempfile from types import SimpleNamespace @@ -66,6 +68,27 @@ def _make_request(tool_name: str = "remote_executor", tool_call_id: str = "tc-1" ) +@contextlib.contextmanager +def _unwritable_outputs_path(): + """Yield an ``outputs_path`` that ``os.makedirs`` cannot create, on any platform. + + The parent component is a regular file, so creating a directory below it + fails with an ``OSError`` subclass everywhere (``NotADirectoryError`` on + POSIX, ``FileNotFoundError`` on Windows) and nothing is written outside the + temporary directory. + + This deliberately avoids expressing "unwritable" as a magic absolute path. + ``/nonexistent/...`` was creatable by root in the CI container, and its + replacement ``/dev/null/...`` relies on ``/dev/null`` being a character + device, which is only true on POSIX -- on Windows it is an ordinary + relative path that ``os.makedirs`` happily creates at the drive root. + """ + with tempfile.TemporaryDirectory() as tmpdir: + blocker = pathlib.Path(tmpdir) / "not-a-directory" + blocker.touch() + yield os.path.join(blocker, "outputs") + + def _tm(content: str = "ok", name: str = "tool", tool_call_id: str = "tc-1") -> ToolMessage: return ToolMessage(content=content, name=name, tool_call_id=tool_call_id) @@ -160,20 +183,15 @@ def test_writes_file_and_returns_virtual_path(self): assert f.read() == "full content here" def test_returns_none_on_invalid_path(self): - # ``/dev/null`` is a character device on both Linux and macOS, so - # ``os.makedirs`` cannot create any subdirectory under it for any - # user (including root). The previously-used ``/nonexistent/...`` - # path was silently created by ``mkdir -p`` when the test process - # ran as root inside the CI container, which made this test fail - # in CI independently of the externalization logic under test. - path = _externalize( - "data", - tool_name="test", - tool_call_id="tc-1", - outputs_path="/dev/null/cannot-mkdir-here", - storage_subdir=".tool-results", - ) - assert path is None + with _unwritable_outputs_path() as outputs_path: + path = _externalize( + "data", + tool_name="test", + tool_call_id="tc-1", + outputs_path=outputs_path, + storage_subdir=".tool-results", + ) + assert path is None def test_txt_extension_for_unknown_tool(self): with tempfile.TemporaryDirectory() as tmpdir: @@ -710,9 +728,10 @@ def test_fallback_when_disk_write_fails(self): mw = ToolOutputBudgetMiddleware(config=config) content = "x" * 500 msg = _tm(content, name="tool") - req = _make_request(outputs_path="/dev/null/cannot-mkdir-here") - result = mw.wrap_tool_call(req, lambda _: msg) + with _unwritable_outputs_path() as outputs_path: + req = _make_request(outputs_path=outputs_path) + result = mw.wrap_tool_call(req, lambda _: msg) assert isinstance(result, ToolMessage) assert "omitted from tool output" in result.content