173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
"""
|
|
Tests for the sandbox wrapper package.
|
|
|
|
The wrapper is pure re-exports of the existing
|
|
``omnigent.inner.sandbox`` surface, so the tests only verify two
|
|
properties: every name the ``__all__`` lists is importable, and the
|
|
wrapped names are the same Python objects as their inner
|
|
counterparts. Behavioral tests for the underlying implementation live
|
|
in ``tests/inner/`` and continue to exercise ``inner.sandbox``
|
|
directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import omnigent.inner.bwrap_sandbox as inner_bwrap
|
|
import omnigent.inner.sandbox as inner_sandbox
|
|
from omnigent import sandbox
|
|
from omnigent.sandbox import bwrap
|
|
|
|
|
|
def test_sandbox_all_symbols_importable() -> None:
|
|
"""
|
|
Every name in ``omnigent.sandbox.__all__`` resolves to an attribute on
|
|
the wrapper module.
|
|
|
|
Catches accidental drift between the ``__all__`` list and the actual
|
|
re-export — the failure mode is "looks fine in the source, breaks at
|
|
`from omnigent.sandbox import X`".
|
|
"""
|
|
for name in sandbox.__all__:
|
|
assert hasattr(sandbox, name), f"omnigent.sandbox missing re-export {name!r}"
|
|
|
|
|
|
def test_sandbox_reexports_are_inner_objects() -> None:
|
|
"""
|
|
The re-exported symbols are the same Python objects as the originals
|
|
in ``omnigent.inner.sandbox``.
|
|
|
|
Identity (``is``) — not equality — because anything else means the
|
|
wrapper has accidentally introduced a parallel definition. Drift here
|
|
would silently break behavior since callers using either import path
|
|
would get different classes and ``isinstance`` checks would fail.
|
|
"""
|
|
assert sandbox.SandboxPolicy is inner_sandbox.SandboxPolicy
|
|
assert sandbox.SandboxBackend is inner_sandbox.SandboxBackend
|
|
assert sandbox.resolve_sandbox is inner_sandbox.resolve_sandbox
|
|
assert sandbox.activate_sandbox is inner_sandbox.activate_sandbox
|
|
assert sandbox.register_backend is inner_sandbox.register_backend
|
|
assert sandbox.with_additional_read_roots is inner_sandbox.with_additional_read_roots
|
|
assert sandbox.with_additional_write_files is inner_sandbox.with_additional_write_files
|
|
assert sandbox.with_additional_write_roots is inner_sandbox.with_additional_write_roots
|
|
assert sandbox.create_private_tmpdir is inner_sandbox.create_private_tmpdir
|
|
assert sandbox.cleanup_private_tmpdir is inner_sandbox.cleanup_private_tmpdir
|
|
assert sandbox.set_temp_env is inner_sandbox.set_temp_env
|
|
assert sandbox.run_launcher is inner_sandbox.run_launcher
|
|
assert sandbox.create_exec_launcher is inner_sandbox.create_exec_launcher
|
|
|
|
|
|
def test_bwrap_all_symbols_importable() -> None:
|
|
"""
|
|
Every name in ``omnigent.sandbox.bwrap.__all__`` resolves on the
|
|
submodule.
|
|
|
|
The bwrap wrapper exists for re-export + side-effecting
|
|
registration, so a typo in ``__all__`` would silently break
|
|
consumers.
|
|
"""
|
|
for name in bwrap.__all__:
|
|
assert hasattr(bwrap, name), f"omnigent.sandbox.bwrap missing re-export {name!r}"
|
|
|
|
|
|
def test_bwrap_reexports_are_inner_objects() -> None:
|
|
"""
|
|
``BwrapSandboxBackend`` re-exports the same identity as
|
|
``omnigent.inner.bwrap_sandbox``.
|
|
|
|
``isinstance`` checks against the wrapper class must succeed for
|
|
the registration side effect to be observable through the
|
|
wrapper.
|
|
"""
|
|
assert bwrap.BwrapSandboxBackend is inner_bwrap.BwrapSandboxBackend
|
|
|
|
|
|
def test_bwrap_import_triggers_backend_registration() -> None:
|
|
"""
|
|
Importing the wrapper module is enough to make the
|
|
``linux_bwrap`` backend resolvable via ``_get_backend``.
|
|
|
|
The contract says importing the wrapper should be functionally
|
|
equivalent to importing ``inner.bwrap_sandbox`` for the purpose of
|
|
side-effecting backend registration. A consumer that switches its
|
|
import path to the wrapper must not lose backend availability.
|
|
"""
|
|
backend = inner_sandbox._get_backend("linux_bwrap")
|
|
assert isinstance(backend, bwrap.BwrapSandboxBackend)
|
|
|
|
|
|
def test_default_sandbox_for_platform_is_bwrap_on_linux_regardless_of_binary() -> None:
|
|
"""
|
|
The Linux platform default is ``linux_bwrap`` and the choice does
|
|
**not** depend on whether the ``bwrap`` binary is installed on the
|
|
host doing the resolving.
|
|
|
|
This is the property that keeps spec parsing host-independent: the
|
|
default is materialized at parse time (loader / spec parser), so a
|
|
CI node or dev laptop without bubblewrap must still be able to load
|
|
a YAML whose ``sandbox:`` block omits ``type:``. The fail-loud
|
|
"no usable mechanism" check is deferred to the backend's runtime
|
|
``resolve()`` (covered by
|
|
``tests/inner/test_bwrap_sandbox.py::test_resolve_raises_when_bwrap_missing``).
|
|
We patch ``shutil.which`` to ``None`` to prove a missing binary
|
|
does not change the selected type.
|
|
"""
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
from omnigent.inner.datamodel import OSEnvSandboxSpec
|
|
|
|
with (
|
|
patch.object(sys, "platform", "linux"),
|
|
patch("omnigent.inner.sandbox.shutil.which", return_value=None),
|
|
):
|
|
spec = inner_sandbox._default_sandbox_for_platform()
|
|
assert isinstance(spec, OSEnvSandboxSpec)
|
|
assert spec.type == "linux_bwrap", (
|
|
"Linux default sandbox should be 'linux_bwrap' independent of "
|
|
f"bwrap availability; got {spec.type!r}."
|
|
)
|
|
|
|
|
|
def test_default_sandbox_for_platform_is_seatbelt_on_macos_regardless_of_binary() -> None:
|
|
"""
|
|
The macOS platform default is ``darwin_seatbelt`` and, like the
|
|
Linux branch, does not probe for ``sandbox-exec`` — selection is a
|
|
pure platform decision so parsing never depends on the resolving
|
|
host's PATH.
|
|
"""
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
from omnigent.inner.datamodel import OSEnvSandboxSpec
|
|
|
|
with (
|
|
patch.object(sys, "platform", "darwin"),
|
|
patch("omnigent.inner.sandbox.shutil.which", return_value=None),
|
|
):
|
|
spec = inner_sandbox._default_sandbox_for_platform()
|
|
assert isinstance(spec, OSEnvSandboxSpec)
|
|
assert spec.type == "darwin_seatbelt", (
|
|
"macOS default sandbox should be 'darwin_seatbelt' independent of "
|
|
f"sandbox-exec availability; got {spec.type!r}."
|
|
)
|
|
|
|
|
|
def test_default_sandbox_for_platform_raises_on_unsupported_platform() -> None:
|
|
"""
|
|
A platform with no sandbox backend at all (anything other than
|
|
Linux or macOS, e.g. Windows) fails loud: there is no backend to
|
|
defer the runtime check to, so the default selector itself raises.
|
|
The operator's explicit opt-out remains
|
|
``os_env.sandbox.type='none'``.
|
|
"""
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
with (
|
|
patch.object(sys, "platform", "win32"),
|
|
pytest.raises(OSError, match="No sandbox backend is available"),
|
|
):
|
|
inner_sandbox._default_sandbox_for_platform()
|