6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
"""Tests for SurfSenseCompactionMiddleware: protected SystemMessage handling and content sanitization."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from langchain_core.messages import (
|
|
AIMessage,
|
|
HumanMessage,
|
|
SystemMessage,
|
|
ToolMessage,
|
|
)
|
|
|
|
from app.agents.chat.shared.middleware.compaction import (
|
|
PROTECTED_SYSTEM_PREFIXES,
|
|
_is_protected_system_message,
|
|
_sanitize_message_content,
|
|
)
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
class TestIsProtectedSystemMessage:
|
|
@pytest.mark.parametrize("prefix", PROTECTED_SYSTEM_PREFIXES)
|
|
def test_each_prefix_protected(self, prefix: str) -> None:
|
|
msg = SystemMessage(content=f"{prefix}\nbody\n</close>")
|
|
assert _is_protected_system_message(msg) is True
|
|
|
|
def test_unprotected_system_message(self) -> None:
|
|
assert (
|
|
_is_protected_system_message(SystemMessage(content="random instructions"))
|
|
is False
|
|
)
|
|
|
|
def test_human_message_never_protected(self) -> None:
|
|
assert (
|
|
_is_protected_system_message(HumanMessage(content="<workspace_tree>..."))
|
|
is False
|
|
)
|
|
|
|
def test_tolerates_leading_whitespace(self) -> None:
|
|
msg = SystemMessage(content=" \n<workspace_tree>\n...")
|
|
assert _is_protected_system_message(msg) is True
|
|
|
|
|
|
class TestSanitizeMessageContent:
|
|
def test_returns_same_message_when_content_present(self) -> None:
|
|
msg = AIMessage(content="hello")
|
|
assert _sanitize_message_content(msg) is msg
|
|
|
|
def test_replaces_none_with_empty_string(self) -> None:
|
|
# Pydantic blocks ``content=None`` at construction; the real
|
|
# crash happens when the streaming layer mutates ``content``
|
|
# after-the-fact. Replicate that by force-setting on a built
|
|
# message.
|
|
msg = AIMessage(
|
|
content="",
|
|
tool_calls=[{"name": "x", "args": {}, "id": "1"}],
|
|
)
|
|
# Bypass pydantic validation to simulate the LiteLLM/Bedrock case
|
|
object.__setattr__(msg, "content", None)
|
|
sanitized = _sanitize_message_content(msg)
|
|
assert sanitized.content == ""
|
|
|
|
|
|
class TestPartitionMessages:
|
|
"""Verify the partition override surfaces protected SystemMessages
|
|
into ``preserved_messages`` regardless of cutoff position.
|
|
"""
|
|
|
|
def _build_partitioner(self):
|
|
# Construct a thin shim — we can't easily instantiate the full
|
|
# SurfSenseCompactionMiddleware without a real model, but the
|
|
# override path needs ``_lc_helper`` to delegate to. We mock
|
|
# that with a simple slicing partitioner equivalent to the real one.
|
|
from app.agents.chat.shared.middleware.compaction import (
|
|
SurfSenseCompactionMiddleware,
|
|
)
|
|
|
|
class _LcHelper:
|
|
@staticmethod
|
|
def _partition_messages(messages, cutoff):
|
|
return messages[:cutoff], messages[cutoff:]
|
|
|
|
class _Stub(SurfSenseCompactionMiddleware):
|
|
def __init__(self):
|
|
self._lc_helper = _LcHelper()
|
|
|
|
return _Stub()
|
|
|
|
def test_protected_system_message_preserved_even_in_summarize_half(self) -> None:
|
|
partitioner = self._build_partitioner()
|
|
protected = SystemMessage(content="<workspace_tree>\n...")
|
|
msgs = [
|
|
HumanMessage(content="old human"),
|
|
AIMessage(content="old ai"),
|
|
protected,
|
|
ToolMessage(content="tool 1", tool_call_id="t1"),
|
|
HumanMessage(content="new"),
|
|
]
|
|
# Cutoff = 4 means everything before index 4 should be summarized
|
|
to_summary, preserved = partitioner._partition_messages(msgs, 4)
|
|
|
|
assert protected not in to_summary
|
|
assert protected in preserved
|
|
# The non-protected old messages remain in to_summary
|
|
assert any(
|
|
isinstance(m, HumanMessage) and m.content == "old human" for m in to_summary
|
|
)
|
|
|
|
def test_unprotected_messages_unaffected(self) -> None:
|
|
partitioner = self._build_partitioner()
|
|
msgs = [
|
|
HumanMessage(content="a"),
|
|
HumanMessage(content="b"),
|
|
HumanMessage(content="c"),
|
|
]
|
|
to_summary, preserved = partitioner._partition_messages(msgs, 2)
|
|
assert [m.content for m in to_summary] == ["a", "b"]
|
|
assert [m.content for m in preserved] == ["c"]
|