supermemory/packages/agent-framework-python/tests/test_utils.py
Dhravya 03773c4f2e
feat(python-sdks): SDK-level cross-source memory deduplication (#1532)
## Stack Context

Part 2 of a 3-PR stack moving memory deduplication into the SDKs. See `sdk-dedup/tools-ts` (parent) for the full context and the TypeScript implementation this mirrors.

## What?

Port the normalized, priority-ordered (`static > dynamic > search`) profile deduplication into the Python SDKs.

- Each request injects one **owned memory block that replaces** the prior block rather than accumulating.
- Dedup is **request-local** (no shared state), so it stays correct under concurrency.

Covers OpenAI, Agent Framework (middleware + context provider), Cartesia, and Pipecat.

## Why?

Keeps the Python SDKs at behavioral parity with the TypeScript SDK so all integrations deduplicate memory the same way.

## Testing

- OpenAI: 31 passed, 11 skipped (live)
- Agent Framework: 59 passed
- Cartesia: 8 passed
- Pipecat: 8 passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes memory formatting and system-prompt injection across multiple SDK integrations; incorrect dedup or replacement could alter LLM context, but there is no auth or data-store risk.
>
> **Overview**
> Ports **normalized cross-source memory deduplication** and **replace-not-append injection** into the Python OpenAI, Agent Framework, Cartesia, and Pipecat packages so they match the TypeScript SDK behavior.
>
> **Deduplication** uses request-local keys: strip optional `[YYYY-MM-DD]` prefixes, normalize whitespace, and compare with `casefold`, with priority **static → dynamic → search**. In **`query` mode**, profile static/dynamic are excluded from dedup input so facts that only appear in search (or overlap profile) are not dropped before formatting.
>
> **Injection** no longer appends memory text every turn. OpenAI and Agent Framework middleware **strip prior owned `<supermemory context="user-memories" readonly>` blocks** and **replace** them once per request while keeping the caller’s system instructions; extra system messages lose stale blocks only. New helpers (`strip`/`replace`/`wrap`) live in each package’s utils.
>
> Tests cover normalized fact variants, query-mode search retention, and stale block replacement.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 42f308b224. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-09-01 06:10:36 +00:00

115 lines
3.9 KiB
Python

"""Tests for utility functions."""
import pytest
from supermemory_agent_framework.utils import (
DeduplicatedMemories,
SimpleLogger,
convert_profile_to_markdown,
create_logger,
deduplicate_memories,
)
class TestDeduplicateMemories:
def test_empty_inputs(self) -> None:
result = deduplicate_memories()
assert result.static == []
assert result.dynamic == []
assert result.search_results == []
def test_static_only(self) -> None:
result = deduplicate_memories(
static=[{"memory": "User likes Python"}],
)
assert result.static == ["User likes Python"]
assert result.dynamic == []
assert result.search_results == []
def test_deduplication_priority(self) -> None:
result = deduplicate_memories(
static=[{"memory": "User likes Python"}],
dynamic=[{"memory": "User likes Python"}, {"memory": "User works remotely"}],
search_results=[{"memory": "User likes Python"}, {"memory": "User prefers async"}],
)
assert result.static == ["User likes Python"]
assert result.dynamic == ["User works remotely"]
assert result.search_results == ["User prefers async"]
def test_string_format(self) -> None:
result = deduplicate_memories(
static=["User likes Python"],
dynamic=["User works remotely"],
)
assert result.static == ["User likes Python"]
assert result.dynamic == ["User works remotely"]
def test_empty_strings_filtered(self) -> None:
result = deduplicate_memories(
static=["", " ", "User likes Python"],
)
assert result.static == ["User likes Python"]
def test_none_items_filtered(self) -> None:
result = deduplicate_memories(
static=[None, {"memory": "valid"}],
)
assert result.static == ["valid"]
def test_normalized_fact_variants(self) -> None:
result = deduplicate_memories(
static=["User likes Python", " user likes python "],
dynamic=["[2026-08-10] USER LIKES PYTHON"],
)
assert result.static == ["User likes Python"]
assert result.dynamic == []
class TestConvertProfileToMarkdown:
def test_empty_profile(self) -> None:
result = convert_profile_to_markdown({"profile": {}})
assert result == ""
def test_static_only(self) -> None:
result = convert_profile_to_markdown(
{"profile": {"static": ["Likes Python", "Lives in SF"]}}
)
assert "## Static Profile" in result
assert "- Likes Python" in result
assert "- Lives in SF" in result
def test_both_sections(self) -> None:
result = convert_profile_to_markdown(
{
"profile": {
"static": ["Likes Python"],
"dynamic": ["Asked about AI"],
}
}
)
assert "## Static Profile" in result
assert "## Dynamic Profile" in result
class TestLogger:
def test_verbose_logger(self, capsys: pytest.CaptureFixture[str]) -> None:
logger = SimpleLogger(verbose=True)
logger.info("test message")
captured = capsys.readouterr()
assert "[supermemory] test message" in captured.out
def test_silent_logger(self, capsys: pytest.CaptureFixture[str]) -> None:
logger = SimpleLogger(verbose=False)
logger.info("test message")
captured = capsys.readouterr()
assert captured.out == ""
def test_error_prefix(self, capsys: pytest.CaptureFixture[str]) -> None:
logger = SimpleLogger(verbose=True)
logger.error("something failed")
captured = capsys.readouterr()
assert "ERROR:" in captured.out
def test_create_logger(self) -> None:
logger = create_logger(True)
assert isinstance(logger, SimpleLogger)