Files
agent-desktop/tests/unit/runtime/test_scroll_recall_gate.py
T

162 lines
6.1 KiB
Python

# -*- coding: utf-8 -*-
# pylint: disable=protected-access
"""The builder's two scroll recall gates.
``_scroll_recall_runnable`` — whether scroll is wired at all. The sandboxed
``recall_history_python`` REPL fails closed without a ``sandbox_config`` (the
governor injects it); the structured ``recall_history`` tool runs in-process
but with no governor the guard layer itself is degraded. So the gate stays
conservative: governor absent and ``allow_unsandboxed`` off → degrade to
native context management rather than evict history nothing can read back.
``_scroll_repl_runnable`` — the narrower gate applied once scroll IS wired:
whether to ALSO offer the Python REPL. It is registered only when a sandbox
actually exists (governor present AND its probe found one) or unsandboxed
recall is explicitly opted in. Otherwise it is omitted — on a no-sandbox
platform it would only fail-closed into a recurring approval prompt — and the
model recalls through the structured ``recall_history`` tool, which needs no
sandbox.
"""
from types import SimpleNamespace
from pineagents.runtime.builder import AgentBuilder
def _agent_config(allow_unsandboxed: bool) -> SimpleNamespace:
return SimpleNamespace(
running=SimpleNamespace(
light_context_config=SimpleNamespace(
scroll_config=SimpleNamespace(
allow_unsandboxed=allow_unsandboxed,
),
),
),
)
def test_runnable_when_governor_present():
# Governor injects sandbox_config, so recall runs regardless of the flag.
cfg = _agent_config(allow_unsandboxed=False)
assert AgentBuilder._scroll_recall_runnable(cfg, governor=object()) is True
def test_runnable_when_unsandboxed_opt_in(monkeypatch):
# No governor, but the deployment opted into unsandboxed recall — which
# needs BOTH the env var and the per-agent flag (agent.json alone can't
# bypass the sandbox).
monkeypatch.setenv("QWENPAW_ALLOW_UNSANDBOXED_RECALL", "1")
cfg = _agent_config(allow_unsandboxed=True)
assert AgentBuilder._scroll_recall_runnable(cfg, governor=None) is True
def test_not_runnable_without_governor_or_optin():
# The reported failure: governor down, no opt-in → recall cannot run.
cfg = _agent_config(allow_unsandboxed=False)
assert AgentBuilder._scroll_recall_runnable(cfg, governor=None) is False
def test_not_runnable_on_malformed_config():
# Missing scroll_config must fail closed (degrade to native), not raise.
cfg = SimpleNamespace(running=SimpleNamespace(light_context_config=None))
assert AgentBuilder._scroll_recall_runnable(cfg, governor=None) is False
# ── recall_history_python REPL gate: only offered when a sandbox exists ──
#
# Scroll is already wired (structured recall_history is present regardless);
# this narrower gate decides whether to ALSO register the sandboxed Python
# REPL. Omitting it on no-sandbox platforms is what removes the recurring
# approval prompt the user reported.
def _governor(sandbox_available: bool) -> SimpleNamespace:
return SimpleNamespace(sandbox_available=sandbox_available)
def test_repl_offered_when_sandbox_available():
cfg = _agent_config(allow_unsandboxed=False)
assert AgentBuilder._scroll_repl_runnable(cfg, _governor(True)) is True
def test_repl_omitted_when_governor_present_but_no_sandbox():
# The reported case: governor is up (so scroll wires and the structured
# tool is present) but the platform has no sandbox (e.g. Windows without
# WSL2). The REPL would only fail-closed → approval popup, so omit it.
cfg = _agent_config(allow_unsandboxed=False)
assert AgentBuilder._scroll_repl_runnable(cfg, _governor(False)) is False
def test_repl_offered_when_unsandboxed_opt_in(monkeypatch):
# No sandbox, but the operator explicitly opted into unsandboxed recall.
monkeypatch.setenv("QWENPAW_ALLOW_UNSANDBOXED_RECALL", "1")
cfg = _agent_config(allow_unsandboxed=True)
assert AgentBuilder._scroll_repl_runnable(cfg, _governor(False)) is True
def test_repl_omitted_without_governor_or_optin():
cfg = _agent_config(allow_unsandboxed=False)
assert AgentBuilder._scroll_repl_runnable(cfg, governor=None) is False
def test_repl_gate_survives_malformed_config():
cfg = SimpleNamespace(running=SimpleNamespace(light_context_config=None))
assert AgentBuilder._scroll_repl_runnable(cfg, governor=None) is False
# ── End-to-end: prove the model's tool list omits the REPL with no sandbox ──
#
# The gate above is the decision; these lock in that the decision actually
# controls REGISTRATION — i.e. with no sandbox the recall_history_python REPL
# never lands in the tool list the model sees, only the structured tool does.
def _identity_wrap(fn, *_args, **_kwargs):
# Bypass the real guard wrapper so we can assert on the raw tools that
# were registered.
return fn
def _scroll(recall_tool, repl_tool) -> SimpleNamespace:
return SimpleNamespace(recall_tool=recall_tool, repl_tool=repl_tool)
def test_no_sandbox_omits_repl_from_registered_tools(monkeypatch):
monkeypatch.setattr(
AgentBuilder,
"_wrap_tool",
staticmethod(_identity_wrap),
)
builder = AgentBuilder.__new__(AgentBuilder)
extra: list = []
builder._append_scroll_recall_tools(
extra,
_scroll("RECALL_HISTORY", "REPL"),
_agent_config(allow_unsandboxed=False),
"ag1",
{},
_governor(sandbox_available=False),
)
# Only the structured tool is registered; the REPL is absent, so the
# model never sees recall_history_python and never hits its approval loop.
assert extra == ["RECALL_HISTORY"]
def test_with_sandbox_registers_both_tools(monkeypatch):
monkeypatch.setattr(
AgentBuilder,
"_wrap_tool",
staticmethod(_identity_wrap),
)
builder = AgentBuilder.__new__(AgentBuilder)
extra: list = []
builder._append_scroll_recall_tools(
extra,
_scroll("RECALL_HISTORY", "REPL"),
_agent_config(allow_unsandboxed=False),
"ag1",
{},
_governor(sandbox_available=True),
)
assert extra == ["RECALL_HISTORY", "REPL"]