Add Handbook.md benchmark tasks
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
"""Pin `sandbox._agent_env`: the single chokepoint that hides /opt/venv from
|
||||
agent-spawned subprocesses (bash)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.tools import sandbox
|
||||
|
||||
|
||||
def _polluted_environ() -> dict[str, str]:
|
||||
return {
|
||||
"HOME": "/home/model",
|
||||
"USER": "model",
|
||||
"LANG": "C.UTF-8",
|
||||
"PATH": "/opt/venv/bin:/app/scripts:/usr/local/bin:/usr/bin:/bin",
|
||||
"VIRTUAL_ENV": "/opt/venv",
|
||||
"UV_PROJECT_ENVIRONMENT": "/opt/venv",
|
||||
"PYTHONPATH": "/app:/opt/venv/lib/python3.13/site-packages",
|
||||
"PYTHONHOME": "/opt/venv",
|
||||
"SETUID": "1000",
|
||||
"SETGID": "1000",
|
||||
}
|
||||
|
||||
|
||||
def test_strips_opt_venv_from_path():
|
||||
with mock.patch.dict(os.environ, _polluted_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert "/opt/venv" not in env["PATH"], env["PATH"]
|
||||
|
||||
|
||||
def test_strips_app_from_path():
|
||||
with mock.patch.dict(os.environ, _polluted_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert "/app" not in env["PATH"], env["PATH"]
|
||||
|
||||
|
||||
def test_drops_venv_pointers():
|
||||
with mock.patch.dict(os.environ, _polluted_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
for var in ("VIRTUAL_ENV", "UV_PROJECT_ENVIRONMENT", "PYTHONPATH", "PYTHONHOME"):
|
||||
assert var not in env, f"{var!r} leaked: {env.get(var)!r}"
|
||||
|
||||
|
||||
def test_drops_privilege_control_vars():
|
||||
with mock.patch.dict(os.environ, _polluted_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert "SETUID" not in env
|
||||
assert "SETGID" not in env
|
||||
|
||||
|
||||
def test_preserves_user_facing_vars():
|
||||
with mock.patch.dict(os.environ, _polluted_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert env["HOME"] == "/home/model"
|
||||
assert env["USER"] == "model"
|
||||
assert env["LANG"] == "C.UTF-8"
|
||||
|
||||
|
||||
def test_path_is_a_concrete_default_not_absent():
|
||||
with mock.patch.dict(os.environ, _polluted_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert env.get("PATH"), "agent PATH unexpectedly empty"
|
||||
assert "/usr/bin" in env["PATH"]
|
||||
assert "/usr/local/bin" in env["PATH"]
|
||||
|
||||
|
||||
# Regression guard for the P1: derived images (docker/stem-*) append dirs like
|
||||
# /opt/conda/bin; sanitising must strip only /opt/venv and /app, never the rest.
|
||||
|
||||
|
||||
def _derived_image_environ() -> dict[str, str]:
|
||||
env = _polluted_environ()
|
||||
env["PATH"] = "/opt/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/conda/bin"
|
||||
return env
|
||||
|
||||
|
||||
def test_preserves_derived_image_path_additions():
|
||||
with mock.patch.dict(os.environ, _derived_image_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
entries = env["PATH"].split(os.pathsep)
|
||||
assert "/opt/conda/bin" in entries, env["PATH"]
|
||||
assert "/opt/venv/bin" not in entries, env["PATH"]
|
||||
|
||||
|
||||
def test_preserves_path_entry_order():
|
||||
with mock.patch.dict(os.environ, _derived_image_environ(), clear=True):
|
||||
env = sandbox._agent_env()
|
||||
entries = env["PATH"].split(os.pathsep)
|
||||
assert entries == ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin", "/opt/conda/bin"]
|
||||
|
||||
|
||||
def test_strips_only_venv_and_app_prefixes_not_lookalikes():
|
||||
polluted = _polluted_environ()
|
||||
polluted["PATH"] = "/opt/venv/bin:/opt/venvtools/bin:/application/bin:/app/scripts:/usr/bin"
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
env = sandbox._agent_env()
|
||||
entries = env["PATH"].split(os.pathsep)
|
||||
assert entries == ["/opt/venvtools/bin", "/application/bin", "/usr/bin"], env["PATH"]
|
||||
|
||||
|
||||
def test_falls_back_to_default_when_only_server_entries():
|
||||
polluted = _polluted_environ()
|
||||
polluted["PATH"] = "/opt/venv/bin:/app/scripts"
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert env["PATH"] == sandbox._AGENT_DEFAULT_PATH
|
||||
|
||||
|
||||
def test_falls_back_to_default_when_path_unset():
|
||||
polluted = _polluted_environ()
|
||||
del polluted["PATH"]
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert env["PATH"] == sandbox._AGENT_DEFAULT_PATH
|
||||
|
||||
|
||||
def test_parent_env_unchanged():
|
||||
# Sanitiser must not mutate os.environ — core itself relies on those
|
||||
# vars for its own subsequent imports.
|
||||
polluted = _polluted_environ()
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
sandbox._agent_env()
|
||||
assert os.environ["PATH"] == polluted["PATH"]
|
||||
assert os.environ["VIRTUAL_ENV"] == polluted["VIRTUAL_ENV"]
|
||||
assert os.environ["UV_PROJECT_ENVIRONMENT"] == polluted["UV_PROJECT_ENVIRONMENT"]
|
||||
assert os.environ["SETUID"] == polluted["SETUID"]
|
||||
|
||||
|
||||
def test_drops_harness_internal_vars():
|
||||
polluted = _polluted_environ()
|
||||
polluted.update(
|
||||
{
|
||||
"WORLDBENCH_ROOT": "/app",
|
||||
"WORLDBENCH_SEED": "deadbeef",
|
||||
"WORLDBENCH_TOOL_SETS": "core_debug",
|
||||
# A harness var nobody listed explicitly — the prefix sweep must catch it.
|
||||
"WORLDBENCH_SOME_FUTURE_VAR": "/app/whatever",
|
||||
"INPUTDIR": "/app/setup_data/entities/core",
|
||||
"OUTPUTDIR": "/app/output_data/core",
|
||||
"BUNDLE_OUTPUT_DIR": "/app/output_data/services",
|
||||
"BUNDLE_INPUT_DIR": "/app/bundle/services/core",
|
||||
"BUNDLEDIR": "/app/bundle",
|
||||
"PORT": "41984",
|
||||
"VIEWER_PORT": "8123",
|
||||
}
|
||||
)
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
env = sandbox._agent_env()
|
||||
for var in (
|
||||
"WORLDBENCH_ROOT",
|
||||
"WORLDBENCH_SEED",
|
||||
"WORLDBENCH_TOOL_SETS",
|
||||
"WORLDBENCH_SOME_FUTURE_VAR",
|
||||
"INPUTDIR",
|
||||
"OUTPUTDIR",
|
||||
"BUNDLE_OUTPUT_DIR",
|
||||
"BUNDLE_INPUT_DIR",
|
||||
"BUNDLEDIR",
|
||||
"PORT",
|
||||
"VIEWER_PORT",
|
||||
):
|
||||
assert var not in env, f"{var!r} leaked to the agent: {env.get(var)!r}"
|
||||
# /app must not survive anywhere in the agent env (paths or otherwise).
|
||||
assert not any("/app" in v for v in env.values()), env
|
||||
|
||||
|
||||
def test_preserves_clock_var():
|
||||
# The fake clock must reach the agent's shell; it is NOT a harness leak.
|
||||
polluted = _polluted_environ()
|
||||
polluted["WORLDBENCH_CURRENT_TIME"] = "2026-01-02T03:04:05Z"
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
env = sandbox._agent_env()
|
||||
assert env["WORLDBENCH_CURRENT_TIME"] == "2026-01-02T03:04:05Z"
|
||||
|
||||
|
||||
def test_privilege_drop_kwargs_noop_when_not_root():
|
||||
# Tests/CI run unprivileged: nothing to drop to, so subprocess gets no
|
||||
# user/group kwargs and runs as the invoking user.
|
||||
with mock.patch("core.tools.sandbox.os.geteuid", return_value=1000):
|
||||
assert sandbox._privilege_drop_kwargs() == {}
|
||||
|
||||
|
||||
def test_privilege_drop_kwargs_drops_when_root():
|
||||
polluted = _polluted_environ() # has SETUID/SETGID = 1000
|
||||
with (
|
||||
mock.patch("core.tools.sandbox.os.geteuid", return_value=0),
|
||||
mock.patch.dict(os.environ, polluted, clear=True),
|
||||
):
|
||||
kwargs = sandbox._privilege_drop_kwargs()
|
||||
assert kwargs == {"user": 1000, "group": 1000, "extra_groups": []}
|
||||
|
||||
|
||||
def test_privilege_drop_kwargs_fails_closed_when_root_without_ids():
|
||||
# Root but no SETUID/SETGID: refuse rather than run the agent's command as
|
||||
# root (a sandbox escape).
|
||||
env = {k: v for k, v in _polluted_environ().items() if k not in ("SETUID", "SETGID")}
|
||||
with (
|
||||
mock.patch("core.tools.sandbox.os.geteuid", return_value=0),
|
||||
mock.patch.dict(os.environ, env, clear=True),
|
||||
pytest.raises(RuntimeError, match="SETUID/SETGID are not both set"),
|
||||
):
|
||||
sandbox._privilege_drop_kwargs()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# agent_stream_file — the unprivileged whole-file reader the viewer streams from
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_agent_stream_file_streams_whole_file(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sandbox, "WORKDIR", str(tmp_path))
|
||||
target = tmp_path / "data.bin"
|
||||
payload = bytes(range(256)) * 1000 # 256 KB, spans many read() calls
|
||||
target.write_bytes(payload)
|
||||
chunks = list(sandbox.agent_stream_file(str(target), chunk_size=4096))
|
||||
assert b"".join(chunks) == payload
|
||||
# Actually streamed in pieces rather than buffered as one blob.
|
||||
assert len(chunks) > 1
|
||||
|
||||
|
||||
def test_agent_stream_file_empty_file_yields_nothing(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sandbox, "WORKDIR", str(tmp_path))
|
||||
target = tmp_path / "empty.bin"
|
||||
target.write_bytes(b"")
|
||||
assert list(sandbox.agent_stream_file(str(target))) == []
|
||||
|
||||
|
||||
def test_agent_stream_file_missing_file_raises(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sandbox, "WORKDIR", str(tmp_path))
|
||||
with pytest.raises(sandbox.AgentReadError):
|
||||
list(sandbox.agent_stream_file(str(tmp_path / "nope.bin")))
|
||||
|
||||
|
||||
def test_agent_stream_file_rejects_fifo(tmp_path, monkeypatch):
|
||||
# A named pipe with no writer would block a reader forever; the helper must
|
||||
# refuse non-regular files instead of hanging. (No writer is opened here, so
|
||||
# this test would deadlock if the guard regressed.)
|
||||
monkeypatch.setattr(sandbox, "WORKDIR", str(tmp_path))
|
||||
fifo = tmp_path / "pipe"
|
||||
os.mkfifo(fifo)
|
||||
with pytest.raises(sandbox.AgentReadError):
|
||||
list(sandbox.agent_stream_file(str(fifo)))
|
||||
|
||||
|
||||
def test_agent_read_window_rejects_fifo(tmp_path, monkeypatch):
|
||||
# Same guard on the windowed reader (backs readFile / viewer / grading).
|
||||
monkeypatch.setattr(sandbox, "WORKDIR", str(tmp_path))
|
||||
fifo = tmp_path / "pipe"
|
||||
os.mkfifo(fifo)
|
||||
with pytest.raises(sandbox.AgentReadError):
|
||||
sandbox.agent_read_window(str(fifo))
|
||||
|
||||
|
||||
def test_agent_read_window_roundtrips(tmp_path):
|
||||
sandbox.WORKDIR = str(tmp_path)
|
||||
p = tmp_path / "data.bin"
|
||||
p.write_bytes(b"0123456789abcdef")
|
||||
size, header, raw, start = sandbox.agent_read_window(str(p), offset=4, limit=5, sniff=3)
|
||||
assert size == 16
|
||||
assert header == b"012"
|
||||
assert raw == b"45678"
|
||||
assert start == 4
|
||||
|
||||
|
||||
def test_agent_read_window_raises_on_missing(tmp_path):
|
||||
sandbox.WORKDIR = str(tmp_path)
|
||||
with pytest.raises(sandbox.AgentReadError):
|
||||
sandbox.agent_read_window(str(tmp_path / "nope.bin"))
|
||||
|
||||
|
||||
def test_agent_list_dir(tmp_path):
|
||||
sandbox.WORKDIR = str(tmp_path)
|
||||
(tmp_path / "a.txt").write_text("hi")
|
||||
(tmp_path / "sub").mkdir()
|
||||
entries = {e["name"]: e for e in sandbox.agent_list_dir(str(tmp_path))}
|
||||
assert entries["a.txt"]["is_dir"] is False
|
||||
assert entries["a.txt"]["size"] == 2
|
||||
assert entries["sub"]["is_dir"] is True
|
||||
|
||||
|
||||
def test_run_in_sandbox_passes_sanitised_env(tmp_path):
|
||||
# End-to-end: catches the failure mode where _agent_env exists but
|
||||
# run_in_sandbox forgot to call it.
|
||||
sandbox.WORKDIR = str(tmp_path)
|
||||
polluted = _polluted_environ()
|
||||
with mock.patch.dict(os.environ, polluted, clear=True):
|
||||
result = sandbox.run_in_sandbox(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
"echo PATH=$PATH; echo VENV=${VIRTUAL_ENV:-}; echo UV=${UV_PROJECT_ENVIRONMENT:-}; echo SU=${SETUID:-}",
|
||||
],
|
||||
timeout=10,
|
||||
)
|
||||
assert result["returncode"] == 0, result
|
||||
out = result["stdout"]
|
||||
assert "/opt/venv" not in out, out
|
||||
assert "/app" not in out, out
|
||||
assert "VENV=\n" in out or out.rstrip().endswith("VENV="), out
|
||||
assert "UV=\n" in out or ("UV=" in out and "/opt/venv" not in out), out
|
||||
assert "SU=\n" in out or out.rstrip().endswith("SU="), out
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Verify HiddenToolFilter: hidden tools excluded from list_tools but still callable."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.server.middleware import Middleware
|
||||
from fastmcp.tools.function_tool import FunctionTool
|
||||
|
||||
|
||||
class HiddenToolFilter(Middleware):
|
||||
async def on_list_tools(self, context, call_next):
|
||||
tool_list = await call_next(context)
|
||||
return [t for t in tool_list if "hidden" not in (t.tags or set())]
|
||||
|
||||
|
||||
def _build_app() -> FastMCP:
|
||||
app = FastMCP("test")
|
||||
|
||||
def visible_tool() -> str:
|
||||
"""A normal tool."""
|
||||
return "visible"
|
||||
|
||||
def hidden_tool() -> str:
|
||||
"""A hidden tool."""
|
||||
return "hidden"
|
||||
|
||||
app.add_tool(FunctionTool.from_function(fn=visible_tool, name="visible_tool"))
|
||||
app.add_tool(FunctionTool.from_function(fn=hidden_tool, name="hidden_tool", tags={"hidden"}))
|
||||
app.add_middleware(HiddenToolFilter())
|
||||
return app
|
||||
|
||||
|
||||
async def main():
|
||||
app = _build_app()
|
||||
async with Client(app) as client:
|
||||
# 1. Hidden tool should NOT appear in list_tools
|
||||
tools = await client.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
assert "visible_tool" in tool_names, f"visible_tool missing from {tool_names}"
|
||||
assert "hidden_tool" not in tool_names, f"hidden_tool should be hidden but found in {tool_names}"
|
||||
print("PASS: hidden tool excluded from list_tools")
|
||||
|
||||
# 2. Hidden tool should still be callable
|
||||
result = await client.call_tool("hidden_tool", {})
|
||||
assert result.content[0].text == "hidden", f"unexpected result: {result}"
|
||||
print("PASS: hidden tool still callable via call_tool")
|
||||
|
||||
print("\nAll checks passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,524 @@
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.tools import sandbox
|
||||
from core.tools.prepare_grading_context import (
|
||||
_extract_docx,
|
||||
_extract_pptx,
|
||||
_extract_text,
|
||||
_extract_xlsx,
|
||||
_format_evidence_as_xml,
|
||||
_get_extension,
|
||||
_is_excluded,
|
||||
_walk_dir,
|
||||
prepareGradingContext,
|
||||
)
|
||||
|
||||
|
||||
class TestHelpers(unittest.TestCase):
|
||||
def test_get_extension(self):
|
||||
assert _get_extension("/foo/bar.txt") == "txt"
|
||||
assert _get_extension("/foo/bar.CSV") == "csv"
|
||||
assert _get_extension("/foo/bar") == ""
|
||||
# Dotfiles like .hidden have no extension per os.path.splitext
|
||||
assert _get_extension("/foo/.hidden") == ""
|
||||
|
||||
def test_is_excluded(self):
|
||||
excl = {"__pycache__", ".git"}
|
||||
assert _is_excluded("__pycache__/foo.pyc", excl)
|
||||
assert _is_excluded("src/__pycache__/foo.pyc", excl)
|
||||
assert _is_excluded(".git/config", excl)
|
||||
assert not _is_excluded("src/main.py", excl)
|
||||
|
||||
def test_walk_dir_sorted(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
os.makedirs(os.path.join(d, "b"))
|
||||
open(os.path.join(d, "b", "z.txt"), "w").close()
|
||||
open(os.path.join(d, "b", "a.txt"), "w").close()
|
||||
open(os.path.join(d, "c.txt"), "w").close()
|
||||
results = _walk_dir(d)
|
||||
names = [os.path.basename(r) for r in results]
|
||||
assert names == ["a.txt", "z.txt", "c.txt"] or results == sorted(results)
|
||||
|
||||
|
||||
class TestExtractText(unittest.TestCase):
|
||||
# _extract_text now takes (raw_bytes, file_size, max_bytes) — the actual file
|
||||
# read happens earlier, as the sandbox user, in prepareGradingContext.
|
||||
def test_extract_text_normal(self):
|
||||
content, method, truncated = _extract_text(b"hello world", 11, 50_000)
|
||||
assert content == "hello world"
|
||||
assert method == "read"
|
||||
assert not truncated
|
||||
|
||||
def test_extract_text_truncated(self):
|
||||
raw = b"a" * 1000
|
||||
content, _method, truncated = _extract_text(raw, len(raw), 100)
|
||||
assert truncated
|
||||
assert content is not None
|
||||
assert "[... truncated at 100 bytes" in content
|
||||
assert "total size: 1,000 bytes]" in content
|
||||
|
||||
def test_extract_text_decodes_non_utf8_lossily(self):
|
||||
# Invalid UTF-8 bytes are replaced, never raise.
|
||||
content, _method, _truncated = _extract_text(b"\xff\xfe bad bytes", 12, 50_000)
|
||||
assert content is not None
|
||||
|
||||
|
||||
class TestFormatEvidenceAsXml(unittest.TestCase):
|
||||
def test_empty_evidence(self):
|
||||
assert _format_evidence_as_xml([]) == ""
|
||||
|
||||
def test_single_file_with_content(self):
|
||||
evidence = [
|
||||
{
|
||||
"path": "/tmp/test.txt",
|
||||
"extension": "txt",
|
||||
"size_bytes": 11,
|
||||
"content": "hello world",
|
||||
"truncated": False,
|
||||
}
|
||||
]
|
||||
xml = _format_evidence_as_xml(evidence, "/tmp")
|
||||
assert '<workspace_files directory="/tmp">' in xml
|
||||
assert 'path="/tmp/test.txt"' in xml
|
||||
assert 'type="txt"' in xml
|
||||
assert 'size_bytes="11"' in xml
|
||||
assert "hello world" in xml
|
||||
assert "</workspace_files>" in xml
|
||||
|
||||
def test_null_content_self_closing(self):
|
||||
evidence = [
|
||||
{
|
||||
"path": "/tmp/test.bin",
|
||||
"extension": "bin",
|
||||
"size_bytes": 100,
|
||||
"content": None,
|
||||
"truncated": False,
|
||||
}
|
||||
]
|
||||
xml = _format_evidence_as_xml(evidence)
|
||||
assert "/>" in xml
|
||||
assert "</file>" not in xml
|
||||
|
||||
def test_special_extension_extraction_method(self):
|
||||
evidence = [
|
||||
{
|
||||
"path": "/tmp/test.pdf",
|
||||
"extension": "pdf",
|
||||
"size_bytes": 5000,
|
||||
"content": "pdf text",
|
||||
"extraction_method": "pypdf",
|
||||
"truncated": False,
|
||||
}
|
||||
]
|
||||
xml = _format_evidence_as_xml(evidence)
|
||||
assert 'extraction_method="pypdf"' in xml
|
||||
|
||||
def test_truncated_attribute(self):
|
||||
evidence = [
|
||||
{
|
||||
"path": "/tmp/big.txt",
|
||||
"extension": "txt",
|
||||
"size_bytes": 100000,
|
||||
"content": "partial...",
|
||||
"truncated": True,
|
||||
}
|
||||
]
|
||||
xml = _format_evidence_as_xml(evidence)
|
||||
assert 'truncated="true"' in xml
|
||||
|
||||
|
||||
class TestPrepareGradingContext(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.original_workdir = sandbox.WORKDIR
|
||||
# prepareGradingContext now confines reads to the sandbox workdir, so
|
||||
# point WORKDIR at the dir under test (mirrors production, where grading
|
||||
# collects the agent's deliverables from WORKDIR).
|
||||
sandbox.WORKDIR = self.temp_dir
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
sandbox.WORKDIR = self.original_workdir
|
||||
|
||||
def _write(self, rel_path: str, content: str = "test") -> str:
|
||||
full = os.path.join(self.temp_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(full), exist_ok=True)
|
||||
with open(full, "w") as f:
|
||||
f.write(content)
|
||||
return full
|
||||
|
||||
async def test_returns_xml_prepended_to_final_output(self):
|
||||
self._write("hello.txt", "hello world")
|
||||
result = await prepareGradingContext(final_output="agent response", directory=self.temp_dir)
|
||||
assert result.startswith("<workspace_files")
|
||||
assert result.endswith("agent response")
|
||||
assert "\n\nagent response" in result
|
||||
|
||||
async def test_no_files_returns_just_final_output(self):
|
||||
result = await prepareGradingContext(final_output="agent response", directory=self.temp_dir)
|
||||
assert result == "agent response"
|
||||
|
||||
async def test_nonexistent_directory_returns_final_output(self):
|
||||
result = await prepareGradingContext(final_output="agent response", directory="/nonexistent/path")
|
||||
assert result == "agent response"
|
||||
|
||||
async def test_default_workdir(self):
|
||||
workdir = tempfile.mkdtemp()
|
||||
sandbox.WORKDIR = workdir
|
||||
with open(os.path.join(workdir, "file.txt"), "w") as f:
|
||||
f.write("test")
|
||||
result = await prepareGradingContext(final_output="output")
|
||||
assert "<workspace_files" in result
|
||||
assert result.endswith("output")
|
||||
shutil.rmtree(workdir)
|
||||
|
||||
async def test_extension_filter(self):
|
||||
self._write("hello.txt", "text")
|
||||
self._write("hello.py", "print('hi')")
|
||||
self._write("hello.csv", "a,b")
|
||||
result = await prepareGradingContext(
|
||||
final_output="output",
|
||||
directory=self.temp_dir,
|
||||
extensions=["txt", "csv"],
|
||||
)
|
||||
assert 'type="txt"' in result
|
||||
assert 'type="csv"' in result
|
||||
assert 'type="py"' not in result
|
||||
|
||||
async def test_unsupported_extension_raises(self):
|
||||
with pytest.raises(ValueError) as ctx:
|
||||
await prepareGradingContext(
|
||||
final_output="output",
|
||||
directory=self.temp_dir,
|
||||
extensions=["exe"],
|
||||
)
|
||||
assert "Unsupported" in str(ctx.value)
|
||||
|
||||
async def test_exclude_patterns(self):
|
||||
self._write("src/main.py", "code")
|
||||
self._write("__pycache__/cache.py", "cached")
|
||||
self._write(".git/config", "git stuff")
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir)
|
||||
assert "main.py" in result
|
||||
assert "cache.py" not in result
|
||||
assert "config" not in result
|
||||
|
||||
async def test_max_files_limit(self):
|
||||
for i in range(10):
|
||||
self._write(f"file{i:02d}.txt", f"content {i}")
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir, max_files=3)
|
||||
assert result.count("<file ") == 3
|
||||
|
||||
async def test_max_files_overflow_is_reported_not_silent(self):
|
||||
# The walk is flat + alphabetical, so a low cap can drop later files.
|
||||
# Those must be surfaced (count + names), not silently dropped, so a
|
||||
# rubric does not wrongly conclude an existing file is missing.
|
||||
for i in range(5):
|
||||
self._write(f"file{i:02d}.txt", f"content {i}")
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir, max_files=2)
|
||||
assert result.count("<file ") == 2
|
||||
assert 'truncated_to_max_files="true"' in result
|
||||
assert 'dropped_file_count="3"' in result
|
||||
assert "<files_omitted_due_to_max_files" in result
|
||||
# The dropped files are the alphabetically-later ones.
|
||||
assert "file04.txt" in result
|
||||
assert result.count("<omitted_file ") == 3
|
||||
|
||||
async def test_no_overflow_has_no_truncation_marker(self):
|
||||
for i in range(3):
|
||||
self._write(f"file{i:02d}.txt", f"content {i}")
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir, max_files=10)
|
||||
assert "truncated_to_max_files" not in result
|
||||
assert "files_omitted_due_to_max_files" not in result
|
||||
|
||||
async def test_overflow_marker_emitted_even_when_no_files_fit(self):
|
||||
# max_files=0: nothing is included, but the dir is non-empty. The grader
|
||||
# must still see that files exist (and were omitted), not a bare output.
|
||||
self._write("only.txt", "content")
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir, max_files=0)
|
||||
assert result.startswith("<workspace_files")
|
||||
assert 'dropped_file_count="1"' in result
|
||||
assert "only.txt" in result
|
||||
|
||||
async def test_unsupported_files_skipped(self):
|
||||
self._write("image.png", "binary")
|
||||
self._write("doc.txt", "text")
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir)
|
||||
assert 'type="txt"' in result
|
||||
# Check for the unsupported file's marker tokens specifically — the
|
||||
# tempdir path itself can contain "png" as a random substring.
|
||||
assert "image.png" not in result
|
||||
assert 'type="png"' not in result
|
||||
|
||||
async def test_oversized_special_file_recorded_not_extracted(self):
|
||||
# A special file over the raw-read cap must be surfaced as evidence (so
|
||||
# the rubric knows it exists) but NOT extracted — extracting truncated
|
||||
# PDF/Office bytes would corrupt, and reading it whole could OOM grading.
|
||||
self._write("big.pdf", "x" * 100)
|
||||
with mock.patch("core.tools.prepare_grading_context.MAX_SPECIAL_FILE_BYTES", 10):
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir)
|
||||
assert 'type="pdf"' in result
|
||||
assert 'size_bytes="100"' in result
|
||||
assert "exceeds" in result and "grading read cap" in result
|
||||
assert 'truncated="true"' in result
|
||||
# content is None -> self-closing tag, and the raw bytes never appear.
|
||||
assert "<big.pdf" not in result
|
||||
|
||||
async def test_special_file_under_cap_is_extracted(self):
|
||||
# A small docx under the cap still flows through normal extraction — the
|
||||
# cap must not block legitimate files.
|
||||
from docx import Document
|
||||
|
||||
path = os.path.join(self.temp_dir, "report.docx")
|
||||
doc = Document()
|
||||
doc.add_paragraph("under-cap evidence")
|
||||
doc.save(path)
|
||||
result = await prepareGradingContext(final_output="output", directory=self.temp_dir)
|
||||
assert 'extraction_method="python-docx"' in result
|
||||
assert "under-cap evidence" in result
|
||||
|
||||
async def test_returns_plain_string(self):
|
||||
self._write("test.txt", "content")
|
||||
result = await prepareGradingContext(final_output="my output", directory=self.temp_dir)
|
||||
assert isinstance(result, str)
|
||||
# Should NOT be JSON
|
||||
assert not result.startswith("{")
|
||||
assert not result.startswith("[")
|
||||
|
||||
async def test_empty_directory_returns_final_output(self):
|
||||
result = await prepareGradingContext(final_output="my output", directory=self.temp_dir)
|
||||
assert result == "my output"
|
||||
|
||||
|
||||
class TestExtractDocx(unittest.TestCase):
|
||||
def _make_docx(self, path: str) -> None:
|
||||
from docx import Document
|
||||
|
||||
doc = Document()
|
||||
doc.add_paragraph("Quarterly summary: revenue up 20%.")
|
||||
table = doc.add_table(rows=2, cols=2)
|
||||
table.cell(0, 0).text = "Metric"
|
||||
table.cell(0, 1).text = "Value"
|
||||
table.cell(1, 0).text = "Revenue"
|
||||
table.cell(1, 1).text = "1000"
|
||||
doc.save(path)
|
||||
|
||||
def test_extracts_paragraphs(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "report.docx")
|
||||
self._make_docx(path)
|
||||
content, method, truncated = _extract_docx(pathlib.Path(path).read_bytes(), 50_000)
|
||||
assert method == "python-docx"
|
||||
assert not truncated
|
||||
assert content is not None
|
||||
assert "revenue up 20%" in content
|
||||
|
||||
def test_includes_table_cells(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "report.docx")
|
||||
self._make_docx(path)
|
||||
content, _method, _truncated = _extract_docx(pathlib.Path(path).read_bytes(), 50_000)
|
||||
# Regression: table cell text used to be dropped (paragraphs only).
|
||||
assert content is not None
|
||||
assert "Metric" in content
|
||||
assert "Revenue" in content
|
||||
assert "1000" in content
|
||||
|
||||
def test_includes_nested_table_cells(self):
|
||||
from docx import Document
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "nested.docx")
|
||||
doc = Document()
|
||||
outer = doc.add_table(rows=1, cols=1)
|
||||
cell = outer.cell(0, 0)
|
||||
cell.paragraphs[0].text = "OuterCell"
|
||||
inner = cell.add_table(rows=1, cols=2)
|
||||
inner.cell(0, 0).text = "InnerA"
|
||||
inner.cell(0, 1).text = "InnerB"
|
||||
doc.save(path)
|
||||
|
||||
content, _method, _truncated = _extract_docx(pathlib.Path(path).read_bytes(), 50_000)
|
||||
assert content is not None
|
||||
# cell.text flattens only direct paragraphs; nested tables must recurse.
|
||||
assert "OuterCell" in content
|
||||
assert "InnerA" in content
|
||||
assert "InnerB" in content
|
||||
|
||||
def test_missing_file_returns_none(self):
|
||||
content, method, truncated = _extract_docx(b"not a real docx", 50_000)
|
||||
assert content is None
|
||||
assert method == "python-docx"
|
||||
assert not truncated
|
||||
|
||||
|
||||
class TestExtractXlsx(unittest.TestCase):
|
||||
def _make_xlsx(self, path: str) -> None:
|
||||
from openpyxl import Workbook
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Data"
|
||||
ws.append(["Name", "Value"])
|
||||
ws.append(["foo", 42])
|
||||
wb.save(path)
|
||||
|
||||
def test_extracts_rows(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "sheet.xlsx")
|
||||
self._make_xlsx(path)
|
||||
content, method, truncated = _extract_xlsx(pathlib.Path(path).read_bytes(), 50_000)
|
||||
assert method == "openpyxl"
|
||||
assert not truncated
|
||||
assert content is not None
|
||||
assert "--- Sheet: Data ---" in content
|
||||
assert "Name\tValue" in content
|
||||
assert "foo\t42" in content
|
||||
|
||||
def test_missing_file_returns_none(self):
|
||||
content, method, truncated = _extract_xlsx(b"not a real xlsx", 50_000)
|
||||
assert content is None
|
||||
assert method == "openpyxl"
|
||||
assert not truncated
|
||||
|
||||
def test_every_sheet_appears_even_when_earlier_sheet_truncated(self):
|
||||
# Regression: a big first sheet used to drain the whole budget, so later
|
||||
# tabs never appeared. The budget is now per-sheet.
|
||||
from openpyxl import Workbook
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "big.xlsx")
|
||||
wb = Workbook()
|
||||
ws1 = wb.active
|
||||
ws1.title = "TB Current"
|
||||
for i in range(2000):
|
||||
ws1.append([f"row{i}", "Trial Balance as of 12/31/2025", i, i * 2, "padding text"])
|
||||
ws2 = wb.create_sheet("TB Prior")
|
||||
ws2.append(["Trial Balance as of 11/30/2025"])
|
||||
ws2.append(["Prior period data", 123])
|
||||
wb.save(path)
|
||||
|
||||
content, _method, truncated = _extract_xlsx(pathlib.Path(path).read_bytes(), 2000)
|
||||
assert truncated
|
||||
assert content is not None
|
||||
assert "--- Sheet: TB Current ---" in content
|
||||
assert "--- Sheet: TB Prior ---" in content
|
||||
# The starved later sheet's data survives, and truncation is attributed.
|
||||
assert "11/30/2025" in content
|
||||
assert "Prior period data\t123" in content
|
||||
assert "'TB Current' truncated" in content
|
||||
|
||||
def test_empty_trailing_rows_do_not_consume_budget(self):
|
||||
# openpyxl read-only dimensions can be inflated; fully-empty rows must be
|
||||
# skipped so they don't burn the budget producing only blank tab output.
|
||||
from openpyxl import Workbook
|
||||
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "sparse.xlsx")
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Sparse"
|
||||
ws.append(["header", "value"])
|
||||
ws.append(["real", 1])
|
||||
# Force a large inflated dimension with empty cells far below.
|
||||
ws.cell(row=5000, column=1, value=None)
|
||||
wb.save(path)
|
||||
|
||||
content, _method, truncated = _extract_xlsx(pathlib.Path(path).read_bytes(), 50_000)
|
||||
assert content is not None
|
||||
assert not truncated
|
||||
assert "header\tvalue" in content
|
||||
assert "real\t1" in content
|
||||
# No run of empty tab-joined rows should appear.
|
||||
assert "\t\t\t" not in content
|
||||
|
||||
|
||||
class TestExtractPptx(unittest.TestCase):
|
||||
def _make_pptx(self, path: str) -> None:
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
|
||||
prs = Presentation()
|
||||
s1 = prs.slides.add_slide(prs.slide_layouts[5])
|
||||
s1.shapes.title.text = "Helix Pricing SLT Deck"
|
||||
# Slide 2: a textbox, a table, and speaker notes.
|
||||
s2 = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
tb = s2.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
|
||||
tb.text_frame.text = "Recommended price: $499"
|
||||
table = s2.shapes.add_table(2, 2, Inches(1), Inches(3), Inches(4), Inches(1)).table
|
||||
table.cell(0, 0).text = "Tier"
|
||||
table.cell(0, 1).text = "Price"
|
||||
table.cell(1, 0).text = "Pro"
|
||||
table.cell(1, 1).text = "499"
|
||||
s2.notes_slide.notes_text_frame.text = "Emphasize ROI in the meeting"
|
||||
prs.save(path)
|
||||
|
||||
def test_extracts_slides_tables_and_notes(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "deck.pptx")
|
||||
self._make_pptx(path)
|
||||
content, method, truncated = _extract_pptx(pathlib.Path(path).read_bytes(), 50_000)
|
||||
assert method == "python-pptx"
|
||||
assert not truncated
|
||||
assert content is not None
|
||||
assert "--- Slide 1 ---" in content
|
||||
assert "--- Slide 2 ---" in content
|
||||
assert "Helix Pricing SLT Deck" in content
|
||||
assert "Recommended price: $499" in content
|
||||
assert "Tier\tPrice" in content
|
||||
assert "Pro\t499" in content
|
||||
assert "[Notes] Emphasize ROI in the meeting" in content
|
||||
|
||||
def test_missing_file_returns_none(self):
|
||||
content, method, truncated = _extract_pptx(b"not a real pptx", 50_000)
|
||||
assert content is None
|
||||
assert method == "python-pptx"
|
||||
assert not truncated
|
||||
|
||||
|
||||
class TestPrepareGradingContextBinary(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_docx_and_xlsx_evidence_assembled(self):
|
||||
from docx import Document
|
||||
from openpyxl import Workbook
|
||||
|
||||
original_workdir = sandbox.WORKDIR
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
sandbox.WORKDIR = d # reads are confined to WORKDIR
|
||||
self.addCleanup(setattr, sandbox, "WORKDIR", original_workdir)
|
||||
doc = Document()
|
||||
doc.add_paragraph("agent wrote this in a docx")
|
||||
doc.save(os.path.join(d, "out.docx"))
|
||||
|
||||
wb = Workbook()
|
||||
wb.active.append(["col", "val"])
|
||||
wb.save(os.path.join(d, "out.xlsx"))
|
||||
|
||||
from pptx import Presentation
|
||||
|
||||
prs = Presentation()
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[5])
|
||||
slide.shapes.title.text = "agent wrote this in a pptx"
|
||||
prs.save(os.path.join(d, "out.pptx"))
|
||||
|
||||
result = await prepareGradingContext(final_output="FINAL", directory=d)
|
||||
assert 'type="docx"' in result
|
||||
assert 'extraction_method="python-docx"' in result
|
||||
assert "agent wrote this in a docx" in result
|
||||
assert 'type="xlsx"' in result
|
||||
assert 'extraction_method="openpyxl"' in result
|
||||
# Regression: a .pptx deliverable used to be silently skipped.
|
||||
assert 'type="pptx"' in result
|
||||
assert 'extraction_method="python-pptx"' in result
|
||||
assert "agent wrote this in a pptx" in result
|
||||
assert result.endswith("FINAL")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Tests for ``core.privilege`` — the workdir-ownership helpers
|
||||
(``ensure_workdir``, ``chown_tree_to_target``) used by ``core.setup``.
|
||||
|
||||
The per-command privilege drop lives in ``core.tools.sandbox`` now (the
|
||||
server stays root and drops each agent command instead of the whole process);
|
||||
its tests are in ``test_agent_env.py``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from core import privilege
|
||||
|
||||
|
||||
def _patch_root(is_root: bool):
|
||||
return mock.patch("core.privilege.os.geteuid", return_value=0 if is_root else 1000)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ensure_workdir
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_ensure_workdir_creates_and_chowns_when_root(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("SETUID", "1000")
|
||||
monkeypatch.setenv("SETGID", "1000")
|
||||
target = tmp_path / "workdir-new"
|
||||
with (
|
||||
_patch_root(True),
|
||||
mock.patch("core.privilege.os.chown") as chown,
|
||||
mock.patch("core.privilege.sandbox.WORKDIR", str(target)),
|
||||
):
|
||||
privilege.ensure_workdir()
|
||||
assert target.is_dir()
|
||||
chown.assert_called_once_with(str(target), 1000, 1000)
|
||||
|
||||
|
||||
def test_ensure_workdir_noop_when_not_root(tmp_path):
|
||||
"""Non-root can't mkdir at / and can't chown, so the whole function is a
|
||||
no-op — WORKDIR must already exist (or the caller will fail loudly)."""
|
||||
target = tmp_path / "workdir-missing"
|
||||
with (
|
||||
_patch_root(False),
|
||||
mock.patch("core.privilege.os.chown") as chown,
|
||||
mock.patch("core.privilege.sandbox.WORKDIR", str(target)),
|
||||
):
|
||||
privilege.ensure_workdir()
|
||||
assert not target.exists()
|
||||
chown.assert_not_called()
|
||||
|
||||
|
||||
def test_ensure_workdir_skips_chown_when_env_vars_unset(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SETUID", raising=False)
|
||||
monkeypatch.delenv("SETGID", raising=False)
|
||||
target = tmp_path / "workdir"
|
||||
with (
|
||||
_patch_root(True),
|
||||
mock.patch("core.privilege.os.chown") as chown,
|
||||
mock.patch("core.privilege.sandbox.WORKDIR", str(target)),
|
||||
):
|
||||
privilege.ensure_workdir()
|
||||
assert target.is_dir()
|
||||
chown.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# chown_tree_to_target
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_tree(root):
|
||||
"""Build a small tree: root/a.txt, root/sub/b.txt, root/sub/c.txt."""
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
(root / "a.txt").write_text("a")
|
||||
(root / "sub").mkdir()
|
||||
(root / "sub" / "b.txt").write_text("b")
|
||||
(root / "sub" / "c.txt").write_text("c")
|
||||
|
||||
|
||||
def test_chown_tree_recursively_when_root(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("SETUID", "1000")
|
||||
monkeypatch.setenv("SETGID", "1000")
|
||||
target = tmp_path / "workdir"
|
||||
_make_tree(target)
|
||||
with _patch_root(True), mock.patch("core.privilege.os.chown") as chown:
|
||||
privilege.chown_tree_to_target(target)
|
||||
paths = sorted(call.args[0] for call in chown.call_args_list)
|
||||
expected = sorted(
|
||||
[
|
||||
str(target),
|
||||
str(target / "a.txt"),
|
||||
str(target / "sub"),
|
||||
str(target / "sub" / "b.txt"),
|
||||
str(target / "sub" / "c.txt"),
|
||||
]
|
||||
)
|
||||
assert paths == expected
|
||||
for call in chown.call_args_list:
|
||||
assert call.args[1:] == (1000, 1000)
|
||||
assert call.kwargs == {"follow_symlinks": False}
|
||||
|
||||
|
||||
def test_chown_tree_noop_when_not_root(tmp_path, monkeypatch):
|
||||
"""Local dev / CI: same env vars, but no real privilege to chown — skip."""
|
||||
monkeypatch.setenv("SETUID", "1000")
|
||||
monkeypatch.setenv("SETGID", "1000")
|
||||
target = tmp_path / "workdir"
|
||||
_make_tree(target)
|
||||
with _patch_root(False), mock.patch("core.privilege.os.chown") as chown:
|
||||
privilege.chown_tree_to_target(target)
|
||||
chown.assert_not_called()
|
||||
|
||||
|
||||
def test_chown_tree_noop_when_env_vars_unset(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("SETUID", raising=False)
|
||||
monkeypatch.delenv("SETGID", raising=False)
|
||||
target = tmp_path / "workdir"
|
||||
_make_tree(target)
|
||||
with _patch_root(True), mock.patch("core.privilege.os.chown") as chown:
|
||||
privilege.chown_tree_to_target(target)
|
||||
chown.assert_not_called()
|
||||
|
||||
|
||||
def test_chown_tree_uses_minus_one_for_missing_uid_or_gid(tmp_path, monkeypatch):
|
||||
"""When only one of SETUID/SETGID is set, the other side is left unchanged
|
||||
via the chown ``-1`` sentinel."""
|
||||
monkeypatch.setenv("SETUID", "1000")
|
||||
monkeypatch.delenv("SETGID", raising=False)
|
||||
target = tmp_path / "workdir"
|
||||
target.mkdir()
|
||||
(target / "f.txt").write_text("x")
|
||||
with _patch_root(True), mock.patch("core.privilege.os.chown") as chown:
|
||||
privilege.chown_tree_to_target(target)
|
||||
for call in chown.call_args_list:
|
||||
assert call.args[1:] == (1000, -1)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
"""Tests for core.setup — copies uploaded context files into the workdir."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from core import setup as setup_mod # aliased to avoid clash with pytest's xunit setup_module fixture
|
||||
from core.tools import sandbox
|
||||
|
||||
|
||||
class SetupTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = Path(tempfile.mkdtemp())
|
||||
self.world_root = self.temp_dir / "world"
|
||||
self.workdir = self.temp_dir / "workdir"
|
||||
self.world_root.mkdir()
|
||||
|
||||
self._original_workdir = sandbox.WORKDIR
|
||||
sandbox.WORKDIR = str(self.workdir)
|
||||
|
||||
def tearDown(self):
|
||||
sandbox.WORKDIR = self._original_workdir
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def _run(self, task_id: str | None = None) -> int:
|
||||
"""Invoke setup.main() in-process and return its exit code (0 on normal return)."""
|
||||
env = {"WORLDBENCH_ROOT": str(self.world_root)}
|
||||
if task_id is not None:
|
||||
env["WORLDBENCH_TASK_ID"] = task_id
|
||||
with mock.patch.dict(os.environ, env, clear=False):
|
||||
os.environ.pop("BUNDLEDIR", None)
|
||||
if task_id is None:
|
||||
os.environ.pop("WORLDBENCH_TASK_ID", None)
|
||||
try:
|
||||
setup_mod.main()
|
||||
return 0
|
||||
except SystemExit as e:
|
||||
return int(e.code or 0)
|
||||
|
||||
def test_copies_task_specific_files_into_workdir(self):
|
||||
"""Task-specific setup_data/{task_id}/files/ is copied into WORKDIR."""
|
||||
task_id = "task-abc"
|
||||
files_dir = self.world_root / "tasks" / "setup_data" / task_id / "files"
|
||||
files_dir.mkdir(parents=True)
|
||||
(files_dir / "james.txt").write_text("james is the CEO")
|
||||
(files_dir / "nested").mkdir()
|
||||
(files_dir / "nested" / "notes.md").write_text("# notes")
|
||||
|
||||
assert self._run(task_id=task_id) == 0
|
||||
|
||||
assert (self.workdir / "james.txt").read_text() == "james is the CEO"
|
||||
assert (self.workdir / "nested" / "notes.md").read_text() == "# notes"
|
||||
|
||||
def test_falls_back_to_generic_setup_data_when_task_dir_missing(self):
|
||||
"""If no task-specific dir exists, copies from {world_root}/setup_data/files/."""
|
||||
generic_files = self.world_root / "setup_data" / "files"
|
||||
generic_files.mkdir(parents=True)
|
||||
(generic_files / "shared.txt").write_text("shared context")
|
||||
|
||||
assert self._run(task_id="unknown-task") == 0
|
||||
assert (self.workdir / "shared.txt").read_text() == "shared context"
|
||||
|
||||
def test_uses_generic_when_no_task_id(self):
|
||||
"""With no WORLDBENCH_TASK_ID set, generic setup_data/files/ is used."""
|
||||
generic_files = self.world_root / "setup_data" / "files"
|
||||
generic_files.mkdir(parents=True)
|
||||
(generic_files / "default.txt").write_text("default")
|
||||
|
||||
assert self._run(task_id=None) == 0
|
||||
assert (self.workdir / "default.txt").read_text() == "default"
|
||||
|
||||
def test_merges_into_existing_workdir(self):
|
||||
"""Existing workdir contents are preserved; new files are added alongside."""
|
||||
self.workdir.mkdir(parents=True)
|
||||
(self.workdir / "preexisting.txt").write_text("keep me")
|
||||
|
||||
generic_files = self.world_root / "setup_data" / "files"
|
||||
generic_files.mkdir(parents=True)
|
||||
(generic_files / "new.txt").write_text("new file")
|
||||
|
||||
assert self._run(task_id=None) == 0
|
||||
assert (self.workdir / "preexisting.txt").read_text() == "keep me"
|
||||
assert (self.workdir / "new.txt").read_text() == "new file"
|
||||
|
||||
def test_no_setup_data_exits_cleanly(self):
|
||||
"""If neither task-specific nor generic setup_data exists, exit 0 without error."""
|
||||
assert self._run(task_id="whatever") == 0
|
||||
|
||||
def test_setup_data_without_files_subdir_is_noop(self):
|
||||
"""setup_data/ with no files/ subdirectory is a no-op (does not error).
|
||||
|
||||
Workdir IS created (ensure_workdir runs unconditionally so subsequent
|
||||
tools have something to chdir into), but no files are copied.
|
||||
"""
|
||||
(self.world_root / "setup_data").mkdir()
|
||||
|
||||
assert self._run(task_id=None) == 0
|
||||
|
||||
def test_rejects_symlink_in_setup_files(self):
|
||||
"""Symlinks in the source tree are refused — they'd let a bundle
|
||||
materialize an arbitrary host path (e.g. /app/tasks) into /workdir
|
||||
and then chown it to the model user."""
|
||||
files_dir = self.world_root / "setup_data" / "files"
|
||||
files_dir.mkdir(parents=True)
|
||||
(files_dir / "real.txt").write_text("ok")
|
||||
# Target doesn't need to exist for the check to fire.
|
||||
os.symlink("/app/tasks", str(files_dir / "rubrics"))
|
||||
|
||||
assert self._run(task_id=None) == 1
|
||||
assert not (self.workdir / "rubrics").exists()
|
||||
assert not (self.workdir / "real.txt").exists() # copytree never ran
|
||||
|
||||
def test_rejects_symlinked_files_dir_root(self):
|
||||
"""A symlinked files/ root is rejected too."""
|
||||
real_dir = self.world_root / "setup_data" / "real_files"
|
||||
real_dir.mkdir(parents=True)
|
||||
(real_dir / "x.txt").write_text("x")
|
||||
os.symlink(str(real_dir), str(self.world_root / "setup_data" / "files"))
|
||||
|
||||
assert self._run(task_id=None) == 1
|
||||
assert not (self.workdir / "x.txt").exists()
|
||||
|
||||
def test_rejects_hardlink_in_setup_files(self):
|
||||
"""Hardlinked files in the source tree are refused — they could
|
||||
point at a locked-down inode (e.g. /app/packages/grading/*) and
|
||||
slip past the symlink guard."""
|
||||
files_dir = self.world_root / "setup_data" / "files"
|
||||
files_dir.mkdir(parents=True)
|
||||
# Two paths sharing one inode: the "target" simulates a protected
|
||||
# file outside the bundle; the entry inside files/ is its hardlink.
|
||||
protected = self.temp_dir / "fake-protected.py"
|
||||
protected.write_text("rubric body")
|
||||
os.link(str(protected), str(files_dir / "rubric.py"))
|
||||
|
||||
assert self._run(task_id=None) == 1
|
||||
assert not (self.workdir / "rubric.py").exists()
|
||||
|
||||
def test_rejects_preexisting_workdir_symlink(self):
|
||||
"""A symlink planted in /workdir before setup runs is refused —
|
||||
otherwise copytree(dirs_exist_ok=True) would write through it as
|
||||
root, into paths the model can't normally write."""
|
||||
files_dir = self.world_root / "setup_data" / "files"
|
||||
files_dir.mkdir(parents=True)
|
||||
(files_dir / "context").mkdir()
|
||||
(files_dir / "context" / "doc.txt").write_text("payload")
|
||||
|
||||
# Plant a model-controlled symlink in /workdir before setup.
|
||||
self.workdir.mkdir(parents=True)
|
||||
protected_target = self.temp_dir / "fake-protected-dir"
|
||||
protected_target.mkdir()
|
||||
os.symlink(str(protected_target), str(self.workdir / "context"))
|
||||
|
||||
assert self._run(task_id=None) == 1
|
||||
# The bundle file must not have been written through the symlink.
|
||||
assert not (protected_target / "doc.txt").exists()
|
||||
|
||||
def test_chowns_workdir_to_target_uid_gid_when_root(self):
|
||||
"""Under (mocked) root, copied files are chowned back to SETUID:SETGID
|
||||
so the unprivileged model user can modify them."""
|
||||
generic_files = self.world_root / "setup_data" / "files"
|
||||
generic_files.mkdir(parents=True)
|
||||
(generic_files / "a.txt").write_text("hello")
|
||||
(generic_files / "sub").mkdir()
|
||||
(generic_files / "sub" / "b.txt").write_text("world")
|
||||
|
||||
env = {"WORLDBENCH_ROOT": str(self.world_root), "SETUID": "1000", "SETGID": "1000"}
|
||||
with (
|
||||
mock.patch.dict(os.environ, env, clear=False),
|
||||
mock.patch("core.privilege.os.geteuid", return_value=0),
|
||||
mock.patch("core.privilege.os.chown") as chown,
|
||||
):
|
||||
os.environ.pop("BUNDLEDIR", None)
|
||||
os.environ.pop("WORLDBENCH_TASK_ID", None)
|
||||
setup_mod.main()
|
||||
|
||||
chowned = sorted(call.args[0] for call in chown.call_args_list)
|
||||
# ensure_workdir chowns WORKDIR; chown_tree_to_target chowns it again plus
|
||||
# every entry under it. We only assert the post-copy entries are present.
|
||||
assert str(self.workdir / "a.txt") in chowned
|
||||
assert str(self.workdir / "sub") in chowned
|
||||
assert str(self.workdir / "sub" / "b.txt") in chowned
|
||||
for call in chown.call_args_list:
|
||||
assert call.args[1:] == (1000, 1000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Tests for the unified-bundle layout in core.setup.
|
||||
|
||||
When ``$BUNDLEDIR/files/`` is mounted (the production harness unpacks the unified
|
||||
trajectory bundle there), core.setup should copy from that path instead
|
||||
of the legacy ``setup_data/files/`` location. We point ``BUNDLEDIR`` at a
|
||||
temp path so the test doesn't depend on the production mount path.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from core import setup as setup_mod # aliased to avoid clash with pytest's xunit setup_module fixture
|
||||
from core.tools import sandbox
|
||||
|
||||
|
||||
class BundleLayoutTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = Path(tempfile.mkdtemp())
|
||||
self.world_root = self.temp_dir / "world"
|
||||
self.workdir = self.temp_dir / "workdir"
|
||||
self.bundle_dir = self.temp_dir / "bundle"
|
||||
self.world_root.mkdir()
|
||||
|
||||
self._env_patch = mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"WORLDBENCH_ROOT": str(self.world_root),
|
||||
"BUNDLEDIR": str(self.bundle_dir),
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
self._env_patch.start()
|
||||
os.environ.pop("WORLDBENCH_TASK_ID", None)
|
||||
|
||||
self._original_workdir = sandbox.WORKDIR
|
||||
sandbox.WORKDIR = str(self.workdir)
|
||||
|
||||
def tearDown(self):
|
||||
sandbox.WORKDIR = self._original_workdir
|
||||
self._env_patch.stop()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_bundle_files_take_precedence_over_legacy_setup_data(self):
|
||||
"""When $BUNDLEDIR/files/ exists, it's used instead of setup_data/files/."""
|
||||
bundle_files = self.bundle_dir / "files"
|
||||
bundle_files.mkdir(parents=True)
|
||||
(bundle_files / "from_bundle.txt").write_text("from bundle")
|
||||
|
||||
legacy_files = self.world_root / "setup_data" / "files"
|
||||
legacy_files.mkdir(parents=True)
|
||||
(legacy_files / "from_legacy.txt").write_text("from legacy")
|
||||
|
||||
setup_mod.main()
|
||||
|
||||
assert (self.workdir / "from_bundle.txt").read_text() == "from bundle"
|
||||
assert not (self.workdir / "from_legacy.txt").exists()
|
||||
|
||||
def test_falls_back_to_legacy_when_bundle_dir_absent(self):
|
||||
"""When BUNDLEDIR points at a path that doesn't exist on disk, the
|
||||
legacy setup_data path still works."""
|
||||
# bundle_dir intentionally not created
|
||||
legacy_files = self.world_root / "setup_data" / "files"
|
||||
legacy_files.mkdir(parents=True)
|
||||
(legacy_files / "from_legacy.txt").write_text("from legacy")
|
||||
|
||||
setup_mod.main()
|
||||
|
||||
assert (self.workdir / "from_legacy.txt").read_text() == "from legacy"
|
||||
|
||||
def test_falls_back_to_legacy_when_bundledir_unset(self):
|
||||
"""When BUNDLEDIR isn't set at all (common local-dev path), the
|
||||
legacy setup_data path is used."""
|
||||
os.environ.pop("BUNDLEDIR", None)
|
||||
legacy_files = self.world_root / "setup_data" / "files"
|
||||
legacy_files.mkdir(parents=True)
|
||||
(legacy_files / "from_legacy.txt").write_text("from legacy")
|
||||
|
||||
setup_mod.main()
|
||||
|
||||
assert (self.workdir / "from_legacy.txt").read_text() == "from legacy"
|
||||
|
||||
def test_bundle_and_legacy_produce_equivalent_workdir(self):
|
||||
"""Same files served via either layout yield the same WORKDIR contents."""
|
||||
contents = {"a.txt": "alpha", "nested/b.md": "# beta"}
|
||||
|
||||
# First run: bundle layout
|
||||
bundle_files = self.bundle_dir / "files"
|
||||
for rel, body in contents.items():
|
||||
p = bundle_files / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(body)
|
||||
|
||||
setup_mod.main()
|
||||
bundle_snapshot = {
|
||||
str(p.relative_to(self.workdir)): p.read_text() for p in self.workdir.rglob("*") if p.is_file()
|
||||
}
|
||||
|
||||
# Second run: same files via legacy layout, fresh workdir
|
||||
shutil.rmtree(self.bundle_dir)
|
||||
shutil.rmtree(self.workdir, ignore_errors=True)
|
||||
legacy_files = self.world_root / "setup_data" / "files"
|
||||
for rel, body in contents.items():
|
||||
p = legacy_files / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(body)
|
||||
|
||||
setup_mod.main()
|
||||
legacy_snapshot = {
|
||||
str(p.relative_to(self.workdir)): p.read_text() for p in self.workdir.rglob("*") if p.is_file()
|
||||
}
|
||||
|
||||
assert bundle_snapshot == legacy_snapshot
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Tests for core's sandbox viewer app — focused on the download path, which
|
||||
streams file bytes from a uid-1000 reader instead of buffering them whole."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from core import viewer
|
||||
from core.tools import sandbox
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
# Point WORKDIR at a temp dir so reads stay confined to it, and run the
|
||||
# viewer with no proxy token so the middleware lets requests through.
|
||||
monkeypatch.setattr(sandbox, "WORKDIR", str(tmp_path))
|
||||
monkeypatch.setattr(viewer, "get_proxy_token", lambda: None)
|
||||
return TestClient(viewer.create_core_viewer_app())
|
||||
|
||||
|
||||
def test_download_streams_file_contents(client, tmp_path):
|
||||
payload = bytes(range(256)) * 1000 # 256 KB — larger than one read() chunk
|
||||
(tmp_path / "data.bin").write_bytes(payload)
|
||||
resp = client.get("/api/download", params={"path": "data.bin"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.content == payload
|
||||
assert resp.headers["content-type"] == "application/octet-stream"
|
||||
assert 'filename="data.bin"' in resp.headers["content-disposition"]
|
||||
|
||||
|
||||
def test_download_empty_file(client, tmp_path):
|
||||
(tmp_path / "empty.bin").write_bytes(b"")
|
||||
resp = client.get("/api/download", params={"path": "empty.bin"})
|
||||
assert resp.status_code == 200
|
||||
assert resp.content == b""
|
||||
|
||||
|
||||
def test_download_missing_file_returns_404(client):
|
||||
resp = client.get("/api/download", params={"path": "nope.bin"})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_download_path_escape_rejected(client):
|
||||
resp = client.get("/api/download", params={"path": "../../etc/passwd"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_download_fifo_returns_404_not_hang(client, tmp_path):
|
||||
# An agent-created named pipe must not block a request worker waiting for a
|
||||
# writer — it's rejected as a non-regular file. (No writer is opened, so a
|
||||
# regression here would hang the test.)
|
||||
os.mkfifo(tmp_path / "pipe")
|
||||
resp = client.get("/api/download", params={"path": "pipe"})
|
||||
assert resp.status_code == 404
|
||||
Reference in New Issue
Block a user