* feat(sdk): expose memory lifecycle status filters Co-authored-by: Cursor <cursoragent@cursor.com> * feat(sdk): persist idempotent write outcomes Co-authored-by: Cursor <cursoragent@cursor.com> * feat(sdk): add explicit exact raw APIs Co-authored-by: Cursor <cursoragent@cursor.com> * feat(sdk): snapshot quiesced local data plane Co-authored-by: Cursor <cursoragent@cursor.com> * feat(sdk): restore quiesced local data plane Co-authored-by: Cursor <cursoragent@cursor.com> * fix(sdk): report incomplete physical deletes Co-authored-by: Cursor <cursoragent@cursor.com> * docs(sdk): record authority migration contracts Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: alvinfei <alvinfei@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
5.3 KiB
Python
135 lines
5.3 KiB
Python
"""所有 cache 后端必须实现同一组**有状态**方法。
|
||
|
||
为什么需要这条守卫:`CacheBase` 给有状态方法留了 no-op 默认实现(`return None` / `return False`),
|
||
好处是新后端不必全量实现就能跑,坏处是**漏实现会静默降级、不报任何错**。
|
||
|
||
实测代价:dev10 的 episode 划分只在 `cache_sqlite` 实现了,而生产用的是 `cache_mysql`,
|
||
于是 `get_episode_state` 永远返回 None → 每次写入都开新 episode。线上 8798 次划分决策
|
||
**全是 `new`、`idle_seconds` 全为 null**,功能等于完全没生效,却一条错误日志都没有。
|
||
|
||
审计类方法(memory_operations / pipeline_logs / metrics)不在此列 —— 它们丢了只影响观测。
|
||
这里只管**会改变功能行为**的状态方法。
|
||
"""
|
||
|
||
import inspect
|
||
|
||
import pytest
|
||
|
||
from hy_memory.data.cache_base import CacheBase
|
||
from hy_memory.data.cache_mysql import MysqlCache
|
||
from hy_memory.data.cache_sqlite import SqliteCache
|
||
|
||
|
||
# 会改变功能行为的有状态方法:漏实现 = 功能静默失效
|
||
STATEFUL_METHODS = [
|
||
"get_episode_state",
|
||
"upsert_episode_state",
|
||
"claim_write_outcome",
|
||
"finish_write_outcome",
|
||
"get_write_outcome",
|
||
]
|
||
|
||
BACKENDS = [
|
||
pytest.param(SqliteCache, id="sqlite"),
|
||
pytest.param(MysqlCache, id="mysql"),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize("backend", BACKENDS)
|
||
@pytest.mark.parametrize("method", STATEFUL_METHODS)
|
||
def test_backend_overrides_stateful_method(backend, method):
|
||
"""后端必须**自己实现**,不能继承基类的 no-op。"""
|
||
assert hasattr(backend, method), f"{backend.__name__} 缺少 {method}"
|
||
own = getattr(backend, method)
|
||
base = getattr(CacheBase, method)
|
||
assert own is not base, (
|
||
f"{backend.__name__}.{method} 直接继承了 CacheBase 的 no-op 默认实现 —— "
|
||
f"该后端上此功能会静默失效(不报错、行为退化)"
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("method", STATEFUL_METHODS)
|
||
def test_signatures_match_base(method):
|
||
"""签名要和基类一致,否则调用方按基类签名传参会 TypeError(且常被 except 吞掉)。"""
|
||
base_sig = inspect.signature(getattr(CacheBase, method))
|
||
for backend in (SqliteCache, MysqlCache):
|
||
sig = inspect.signature(getattr(backend, method))
|
||
assert list(sig.parameters) == list(base_sig.parameters), (
|
||
f"{backend.__name__}.{method} 签名与 CacheBase 不一致:\n"
|
||
f" base: {base_sig}\n impl: {sig}"
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("backend", BACKENDS)
|
||
def test_episode_state_table_declared(backend):
|
||
"""建表 DDL 里必须有 episode_state —— 方法实现了但表没建同样会静默失效。"""
|
||
import hy_memory.data.cache_mysql as m
|
||
import hy_memory.data.cache_sqlite as s
|
||
|
||
mod = s if backend is SqliteCache else m
|
||
src = inspect.getsource(mod)
|
||
assert "CREATE TABLE IF NOT EXISTS episode_state" in src, (
|
||
f"{mod.__name__} 未声明 episode_state 建表语句"
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("backend", BACKENDS)
|
||
def test_write_outcomes_table_declared(backend):
|
||
import hy_memory.data.cache_mysql as m
|
||
import hy_memory.data.cache_sqlite as s
|
||
|
||
mod = s if backend is SqliteCache else m
|
||
src = inspect.getsource(mod)
|
||
assert "CREATE TABLE IF NOT EXISTS write_outcomes" in src
|
||
|
||
|
||
class TestRollingSummaryFullyRemoved:
|
||
"""rolling summary 已整体移除,残留引用会让应用在启动路径报错。"""
|
||
|
||
@pytest.mark.parametrize("method", [
|
||
"get_summary_buffer", "upsert_summary_buffer", "clear_summary_buffer",
|
||
])
|
||
def test_summary_buffer_methods_gone(self, method):
|
||
for cls in (CacheBase, SqliteCache, MysqlCache):
|
||
assert not hasattr(cls, method), f"{cls.__name__} 仍有 {method}"
|
||
|
||
def test_summary_buffer_table_gone(self):
|
||
import hy_memory.data.cache_mysql as m
|
||
import hy_memory.data.cache_sqlite as s
|
||
|
||
for mod in (s, m):
|
||
assert "summary_buffer" not in inspect.getsource(mod), (
|
||
f"{mod.__name__} 仍有 summary_buffer 建表/查询"
|
||
)
|
||
|
||
def test_modules_gone(self):
|
||
for name in ("hy_memory.agent.rolling_summary", "hy_memory.agent.summarizer"):
|
||
with pytest.raises(ImportError):
|
||
__import__(name)
|
||
|
||
def test_enable_summary_switch_gone(self):
|
||
from hy_memory.config import LLMConfig
|
||
from hy_memory.pipelines.base import WriteRequest
|
||
|
||
assert not hasattr(LLMConfig(), "enable_summary")
|
||
assert not hasattr(LLMConfig(), "summary_buffer_window")
|
||
assert not hasattr(WriteRequest(), "enable_summary")
|
||
|
||
def test_public_api_param_gone(self):
|
||
from hy_memory.client import HyMemoryClient
|
||
|
||
for fn in ("add", "async_add"):
|
||
if not hasattr(HyMemoryClient, fn):
|
||
continue
|
||
params = inspect.signature(getattr(HyMemoryClient, fn)).parameters
|
||
assert "enable_summary" not in params, f"client.{fn} 仍有 enable_summary"
|
||
|
||
def test_mem_agent_summary_path_gone(self):
|
||
from hy_memory.agent.mem_agent import MemAgent, ProcessMode
|
||
|
||
assert not hasattr(ProcessMode, "SUMMARY_ONLY")
|
||
params = inspect.signature(MemAgent.process_add).parameters
|
||
assert "enable_summary" not in params
|
||
src = inspect.getsource(MemAgent)
|
||
assert "self.summarizer" not in src
|