Files
wehub-resource-sync 25576b0be6
Checks (magui2.0) / python-lint (push) Failing after 1s
Checks (magui2.0) / python-test (push) Failing after 0s
Checks (magui2.0) / frontend-lint (push) Failing after 0s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
Checks (magui2.0) / python-format (push) Failing after 1s
CodeQL Advanced / Analyze (python) (push) Failing after 0s
Checks (magui2.0) / python-pyright (push) Failing after 1s
Checks (magui2.0) / frontend-format (push) Failing after 1s
Checks (magui2.0) / frontend-typecheck (push) Failing after 0s
Checks (magui2.0) / frontend-test (push) Failing after 2s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:24:32 +08:00

263 lines
8.6 KiB
Python

"""Tests for fara state I/O — message round-trip serialization + trim integration."""
from __future__ import annotations
import json
import pytest
from PIL import Image
from magentic_ui.agents.web_surfer.fara._fara_qwen3 import (
FaraQwen3Agent,
FaraQwen3AgentConfig,
FaraQwen3AgentState,
)
from magentic_ui.agents.web_surfer.fara._state_io import (
message_from_dict,
message_to_dict,
)
from magentic_ui.agents.web_surfer.fara._types import ImageObj, LLMMessage
def _img(color: str = "red") -> ImageObj:
return ImageObj.from_pil(Image.new("RGB", (8, 8), color))
def test_text_only_message_round_trips():
msg = LLMMessage(role="user", content="hello world", metadata={"k": "v"})
out = message_from_dict(message_to_dict(msg))
assert out.role == "user"
assert out.content == "hello world"
assert out.metadata == {"k": "v"}
def test_assistant_text_message_no_metadata():
msg = LLMMessage(role="assistant", content="reply")
out = message_from_dict(message_to_dict(msg))
assert out.role == "assistant"
assert out.content == "reply"
assert out.metadata is None
def test_system_message_round_trips():
msg = LLMMessage(role="system", content="you are a helpful agent")
out = message_from_dict(message_to_dict(msg))
assert out.role == "system"
assert out.content == "you are a helpful agent"
def test_image_plus_text_round_trips():
img = _img("blue")
msg = LLMMessage(
role="user",
content=[img, "describe this"],
metadata={"is_original": True},
)
out = message_from_dict(message_to_dict(msg))
assert out.role == "user"
assert out.metadata == {"is_original": True}
assert isinstance(out.content, list)
assert len(out.content) == 2
restored_img, restored_text = out.content
assert isinstance(restored_img, ImageObj)
assert restored_img.image.size == (8, 8)
# Pixel data preserved
assert restored_img.image.getpixel((0, 0)) == (0, 0, 255)
assert restored_text == "describe this"
def test_multiple_images_in_one_message():
msg = LLMMessage(
role="user",
content=[_img("red"), "first", _img("green"), "second"],
)
out = message_from_dict(message_to_dict(msg))
assert isinstance(out.content, list)
assert len(out.content) == 4
assert isinstance(out.content[0], ImageObj)
assert out.content[1] == "first"
assert isinstance(out.content[2], ImageObj)
assert out.content[3] == "second"
# Distinct pixel values prove we didn't share encoded bytes
assert out.content[0].image.getpixel((0, 0)) == (255, 0, 0)
assert out.content[2].image.getpixel((0, 0)) == (0, 128, 0)
def test_dict_form_is_json_serializable():
msg = LLMMessage(role="user", content=[_img(), "x"], metadata={"a": 1})
encoded = message_to_dict(msg)
# Round-trip through json.dumps to confirm no non-serializable bits leaked
text = json.dumps(encoded)
decoded = json.loads(text)
out = message_from_dict(decoded)
assert isinstance(out.content, list)
assert isinstance(out.content[0], ImageObj)
assert out.content[1] == "x"
def test_unknown_part_type_is_skipped():
payload = {
"role": "user",
"content": [
{"type": "text", "text": "ok"},
{"type": "wat", "data": "ignored"},
],
"metadata": None,
}
out = message_from_dict(payload)
assert isinstance(out.content, list)
assert out.content == ["ok"]
def test_missing_content_falls_back_to_empty_string():
out = message_from_dict({"role": "assistant"})
assert out.role == "assistant"
assert out.content == ""
def test_missing_role_defaults_to_user():
out = message_from_dict({"content": "hi"})
assert out.role == "user"
assert out.content == "hi"
def test_image_b64_part_without_data_is_skipped():
payload = {
"role": "user",
"content": [{"type": "image_b64"}, {"type": "text", "text": "kept"}],
}
out = message_from_dict(payload)
assert isinstance(out.content, list)
assert out.content == ["kept"]
@pytest.mark.parametrize("role", ["user", "assistant", "system"])
def test_round_trip_preserves_role(role):
msg = LLMMessage(role=role, content="x") # type: ignore[arg-type]
assert message_from_dict(message_to_dict(msg)).role == role
# ---------------------------------------------------------------------------
# Integration with FaraQwen3Agent.maybe_remove_old_screenshots
# ---------------------------------------------------------------------------
def _agent(max_n_images: int = 3) -> FaraQwen3Agent:
"""Construct an agent without initialize() — only the trim helpers are used."""
cfg = FaraQwen3AgentConfig(
client_config={"model": "x", "base_url": "http://localhost:0/v1"},
max_n_images=max_n_images,
)
a = FaraQwen3Agent(cfg)
a._state = FaraQwen3AgentState()
return a
def _user_with_screenshot(text: str, color: str, **meta) -> LLMMessage:
return LLMMessage(
role="user",
content=[_img(color), text],
metadata=meta or None,
)
def _build_long_history() -> list[LLMMessage]:
"""5 turns of [user(screenshot), assistant(text)] + a system header."""
history: list[LLMMessage] = [
LLMMessage(role="system", content="you are an agent"),
_user_with_screenshot("first task", "red", is_original=True),
]
for i in range(4):
history.append(LLMMessage(role="assistant", content=f"step {i}"))
history.append(_user_with_screenshot(f"obs {i}", "blue"))
return history
def _count_images(history: list[LLMMessage]) -> int:
n = 0
for m in history:
if isinstance(m.content, list):
n += sum(1 for c in m.content if isinstance(c, ImageObj))
return n
def test_trim_then_round_trip_preserves_image_count():
agent = _agent(max_n_images=3)
history = _build_long_history()
assert _count_images(history) == 5 # before trim
trimmed = agent.maybe_remove_old_screenshots(history, includes_current=True)
assert _count_images(trimmed) == 3 # max_n_images cap
encoded = [message_to_dict(m) for m in trimmed]
decoded = [message_from_dict(d) for d in encoded]
assert _count_images(decoded) == 3
assert len(decoded) == len(trimmed)
# Roles preserved in order
assert [m.role for m in decoded] == [m.role for m in trimmed]
def test_trim_preserves_original_user_text_after_trim_and_round_trip():
"""Old user message marked is_original keeps its text even when image is trimmed."""
agent = _agent(max_n_images=2)
history = _build_long_history() # is_original on the first user message
trimmed = agent.maybe_remove_old_screenshots(history, includes_current=True)
decoded = [message_from_dict(message_to_dict(m)) for m in trimmed]
# Find the is_original message in the decoded history
originals = [m for m in decoded if (m.metadata or {}).get("is_original") is True]
assert originals, "is_original user message must survive trim+round-trip"
msg = originals[0]
# Image was stripped (it's old), but text remains
assert isinstance(msg.content, list)
assert all(not isinstance(c, ImageObj) for c in msg.content)
assert "first task" in msg.content
def test_round_trip_then_trim_matches_trim_then_round_trip():
"""Order independence: trim and serialization commute for our test history."""
agent = _agent(max_n_images=3)
history = _build_long_history()
a = agent.maybe_remove_old_screenshots(
[message_from_dict(message_to_dict(m)) for m in history],
includes_current=True,
)
b = [
message_from_dict(message_to_dict(m))
for m in agent.maybe_remove_old_screenshots(history, includes_current=True)
]
assert _count_images(a) == _count_images(b) == 3
assert [m.role for m in a] == [m.role for m in b]
def test_full_state_file_shape_and_size():
"""Encoded shape is JSON-serializable and structurally well-formed."""
agent = _agent(max_n_images=3)
trimmed = agent.maybe_remove_old_screenshots(
_build_long_history(), includes_current=True
)
payload = {
"last_url": "https://example.com/page",
"scroll": [0, 1234],
"chat_history": [message_to_dict(m) for m in trimmed],
"facts": ["fact A", "fact B"],
}
blob = json.dumps(payload)
parsed = json.loads(blob)
assert parsed["last_url"] == "https://example.com/page"
assert parsed["scroll"] == [0, 1234]
assert isinstance(parsed["chat_history"], list)
assert parsed["facts"] == ["fact A", "fact B"]
restored = [message_from_dict(d) for d in parsed["chat_history"]]
assert _count_images(restored) == 3