test: cover the image gate

The gate has four entry points and each now has a test: `view_image` and
`fetch_local` refuse with the message naming the config key, the registry drops
a plugin tool's picture and says why, and the workspace helpers keep `None` (no
host verdict) permissive. On the CLI side the verdict is asserted to follow the
selected model, and to reach the tools once an instance is bound.
This commit is contained in:
2026-10-21 21:39:33 +08:00
parent da6f4b7dd7
commit 37e9395ac5
5 changed files with 121 additions and 1 deletions
+19 -1
View File
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, override
import pytest
from plyngent.agent import ToolOutput
from plyngent.tools.context import read_only_context
from plyngent.tools.context import get_instance, read_only_context
from plyngent.tools.danger import classify_danger
from plyngent.tools.net import fetch_local, grant_private_host
from plyngent.tools.net.policy import (
@@ -225,6 +225,24 @@ async def test_an_image_response_comes_back_as_the_picture(workspace: Path, http
assert part.image_url.url.startswith("data:image/png;base64,")
async def test_a_text_only_model_keeps_the_metadata_and_the_reason(
workspace: Path,
http_server: str,
) -> None:
"""The picture stays behind, and the model is told why (``image_in``)."""
grant_private_host("127.0.0.1", int(http_server.rsplit(":", 1)[1]))
instance = get_instance()
assert instance is not None
instance.workspace.image_in = False
instance.workspace.image_model = ("ds", "text-only")
output = await fetch_local.handler(f"{http_server}/png")
assert isinstance(output, str)
assert "body_kind=image" in output
assert "error: model 'text-only' does not accept image input" in output
async def test_fetch_read_only_context_blocks_non_get(workspace: Path, http_server: str) -> None:
"""In a read-only context fetch_local only allows GET; outside it POST works."""
del workspace
+16
View File
@@ -6,6 +6,7 @@ import base64
from typing import TYPE_CHECKING
from plyngent.agent import ToolOutput
from plyngent.tools.context import get_instance
from plyngent.tools.file.read import read_file
from plyngent.tools.file.view_image import view_image
@@ -60,6 +61,21 @@ async def test_a_missing_file_is_an_error(workspace: Path) -> None:
assert await view_image.handler("nope.png") == "error: file not found: nope.png"
async def test_a_text_only_model_gets_the_reason_instead_of_the_picture(workspace: Path) -> None:
"""``image_in`` gates the tool exactly like an ``@path`` mention."""
(workspace / "shot.png").write_bytes(PNG)
instance = get_instance()
assert instance is not None
instance.workspace.image_in = False
instance.workspace.image_model = ("deepseek", "deepseek-v4-pro")
result = await view_image.handler("shot.png")
assert isinstance(result, str)
assert result.startswith("error: model 'deepseek-v4-pro' does not accept image input")
assert 'set image_in = true under [providers.deepseek.models."deepseek-v4-pro"]' in result
async def test_read_file_points_at_view_image_for_a_picture(workspace: Path) -> None:
(workspace / "shot.png").write_bytes(PNG)
+30
View File
@@ -9,10 +9,12 @@ from plyngent.tools import (
check_command_allowed,
get_trash_enabled,
get_workspace_root,
image_input_refusal,
mode_covers,
parse_access_mode,
resolve_path,
set_command_denylist,
set_image_in,
set_path_denylist,
set_trash_enabled,
)
@@ -67,6 +69,34 @@ def test_trash_switch(workspace: object) -> None:
assert get_trash_enabled() is True
def test_image_switch(workspace: object) -> None:
"""A picture is refused once the host says the model cannot take one."""
del workspace
assert image_input_refusal() is None
set_image_in(image_in=False)
unnamed = image_input_refusal()
assert unnamed is not None
assert "does not accept image input" in unnamed
set_image_in(image_in=False, provider="ds", model="text-only")
named = image_input_refusal()
assert named is not None
assert "model 'text-only' does not accept image input" in named
assert '[providers.ds.models."text-only"]' in named
set_image_in(image_in=True, provider="ds", model="vision")
assert image_input_refusal() is None
def test_the_image_switch_needs_a_declaration(workspace: object) -> None:
"""No host verdict (``None``) sends the picture: only ``False`` refuses."""
del workspace
policy = active_workspace_policy()
assert policy.image_in is None
assert image_input_refusal() is None
def test_config_grant_modes(workspace: object, tmp_path_factory: pytest.TempPathFactory) -> None:
del workspace
outside = tmp_path_factory.mktemp("granted")