Files
Hy-Memory/tests/test_basic_profile_meta.py
T
2026-10-16 16:26:27 +08:00

129 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""L0 基础画像层的 client_meta 透传。
线上实测发现:一次写入产出的多条记忆里,l2_fact 能带回 meta,
l0_basic_info 拿不到——画像走独立的 upsert 路径,透传时漏了。
这组测试锁住修复后的行为。
"""
import asyncio
from hy_memory.agent.tools.basic_profile import upsert_basic_profile
from hy_memory.models.memory import MemoryLayer, MemoryStatus
class FakeVectorStore:
def __init__(self, nodes=None):
self.nodes = list(nodes or [])
self.upserted = []
self.payload_updates = []
async def list_by_user(self, **kwargs):
return list(self.nodes)
async def get_by_id(self, node_id):
return next((n for n in self.nodes if n.node_id == node_id), None)
async def upsert(self, node):
self.upserted.append(node)
self.nodes.append(node)
async def update_payload(self, node_id, updates):
self.payload_updates.append((node_id, updates))
class FakeEmbedService:
async def embed_queued(self, text):
return [0.1] * 8
FIELDS = ["name", "age", "location"]
def upsert(store, kv, meta=None, user="u1"):
return asyncio.run(
upsert_basic_profile(
user_id=user,
kv=kv,
vector_store=store,
embed_service=FakeEmbedService(),
allowed_fields=FIELDS,
client_meta=meta,
)
)
class TestBasicProfileClientMeta:
def test_meta_attached_to_new_node(self):
store = FakeVectorStore()
meta = {"source": "codebuddy", "trace_id": "t-1"}
r = upsert(store, {"name": "张伟"}, meta=meta)
assert r.success and r.changed
assert len(store.upserted) == 1
node = store.upserted[0]
assert node.layer == MemoryLayer.L0_BASIC_INFO
assert node.custom["meta"] == [meta]
def test_basic_info_kv_still_stored_alongside_meta(self):
"""meta 不能挤掉原有的 basic_info_kv——后者是画像装配的依据。"""
store = FakeVectorStore()
upsert(store, {"name": "张伟", "age": 35}, meta={"k": "v"})
custom = store.upserted[0].custom
assert custom["basic_info_kv"] == {"name": "张伟", "age": 35}
assert custom["meta"] == [{"k": "v"}]
def test_no_meta_key_when_not_provided(self):
"""不传 meta 时不应凭空造出该键,否则旧数据形态被改。"""
store = FakeVectorStore()
upsert(store, {"name": "张伟"})
assert "meta" not in store.upserted[0].custom
def test_empty_meta_treated_as_absent(self):
store = FakeVectorStore()
upsert(store, {"name": "张伟"}, meta={})
assert "meta" not in store.upserted[0].custom
def test_meta_is_copied_not_referenced(self):
"""存进去后调用方再改自己的 dict,不应影响已落库的内容。"""
store = FakeVectorStore()
meta = {"source": "a"}
upsert(store, {"name": "张伟"}, meta=meta)
meta["source"] = "mutated"
assert store.upserted[0].custom["meta"] == [{"source": "a"}]
def test_new_version_carries_only_current_meta(self):
"""L0 每次变更新建节点、旧节点转 SUPERSEDED,与 SUPERSEDE 同构:
新节点只带本次 meta,不继承旧链,避免长链上无限累积。"""
store = FakeVectorStore()
upsert(store, {"name": "张伟"}, meta={"round": 1})
upsert(store, {"name": "张伟", "age": 35}, meta={"round": 2})
assert len(store.upserted) == 2
assert store.upserted[1].custom["meta"] == [{"round": 2}]
# 旧节点被标记取代,其 meta 原样留在链上
assert store.payload_updates
old_id, updates = store.payload_updates[-1]
assert old_id == store.upserted[0].node_id
assert updates["status"] == MemoryStatus.SUPERSEDED.value
assert store.upserted[0].custom["meta"] == [{"round": 1}]
def test_no_diff_writes_nothing_even_with_meta(self):
"""内容无变化时本就不落新版本,带了 meta 也不应因此凭空建节点。"""
store = FakeVectorStore()
upsert(store, {"name": "张伟"}, meta={"round": 1})
r = upsert(store, {"name": "张伟"}, meta={"round": 2})
assert r.success and not r.changed
assert len(store.upserted) == 1