Files
Ombre_Brain/tests/test_feel_flow.py

251 lines
8.8 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.
# ============================================================
# Test 3: Feel Flow — end-to-end feel pipeline test
# 测试 3Feel 流程 —— 端到端 feel 管道测试
#
# Tests the complete feel lifecycle:
# 1. hold(content, feel=True) → creates feel bucket
# 2. breath(domain="feel") → retrieves feel buckets by time
# 3. source_bucket marked as digested
# 4. dream() → returns feel crystallization hints
# 5. trace() → can modify/hide feel
# 6. Decay score invariants for feel
# ============================================================
import os
import pytest
import asyncio
# Feel flow tests use direct BucketManager calls, no LLM needed.
@pytest.fixture
async def isolated_tools(test_config, tmp_path, monkeypatch):
"""
Import server tools with config pointing to temp dir.
This avoids touching real data.
"""
# Override env so server.py uses our temp buckets
monkeypatch.setenv("OMBRE_BUCKETS_DIR", str(tmp_path / "buckets"))
# Create directory structure
import os
bd = str(tmp_path / "buckets")
for d in ["permanent", "dynamic", "archive", "dynamic/feel"]:
os.makedirs(os.path.join(bd, d), exist_ok=True)
# Write a minimal config.yaml
import yaml
config_path = str(tmp_path / "config.yaml")
with open(config_path, "w") as f:
yaml.dump(test_config, f)
monkeypatch.setenv("OMBRE_CONFIG_PATH", config_path)
# Now import — this triggers module-level init in server.py
# We need to re-import with our patched env
import importlib
import utils
importlib.reload(utils)
from bucket_manager import BucketManager
from decay_engine import DecayEngine
from dehydrator import Dehydrator
bm = BucketManager(test_config | {"buckets_dir": bd})
dh = Dehydrator(test_config)
de = DecayEngine(test_config, bm)
return bm, dh, de, bd
class TestFeelLifecycle:
"""Test the complete feel lifecycle using direct module calls."""
@pytest.mark.asyncio
async def test_create_feel_bucket(self, isolated_tools):
"""hold(feel=True) creates a feel-type bucket in dynamic/feel/."""
bm, dh, de, bd = isolated_tools
bid = await bm.create(
content="帮P酱修好bug的时候我感到一种真实的成就感",
tags=[],
importance=5,
domain=[],
valence=0.85,
arousal=0.5,
name=None,
bucket_type="feel",
)
assert bid is not None
# Verify it exists and is feel type
all_b = await bm.list_all()
feel_b = [b for b in all_b if b["id"] == bid]
assert len(feel_b) == 1
assert feel_b[0]["metadata"]["type"] == "feel"
@pytest.mark.asyncio
async def test_feel_in_feel_directory(self, isolated_tools):
"""Feel bucket stored under feel/沉淀物/."""
bm, dh, de, bd = isolated_tools
import os
bid = await bm.create(
content="这是一条 feel 测试",
tags=[], importance=5, domain=[],
valence=0.5, arousal=0.3,
name=None, bucket_type="feel",
)
feel_dir = os.path.join(bd, "feel", "沉淀物")
files = os.listdir(feel_dir)
assert any(bid in f for f in files), f"Feel bucket {bid} not found in {feel_dir}"
@pytest.mark.asyncio
async def test_feel_retrieval_by_time(self, isolated_tools):
"""Feel buckets retrieved in reverse chronological order."""
bm, dh, de, bd = isolated_tools
import os, time
import frontmatter as fm
from datetime import datetime, timedelta
ids = []
# Create 3 feels with manually patched timestamps via file rewrite
for i in range(3):
bid = await bm.create(
content=f"Feel #{i+1}",
tags=[], importance=5, domain=[],
valence=0.5, arousal=0.3,
name=None, bucket_type="feel",
)
ids.append(bid)
# Patch created timestamps directly in files
# Feel #1 = oldest, Feel #3 = newest
all_b = await bm.list_all()
for b in all_b:
if b["metadata"].get("type") != "feel":
continue
fpath = bm._find_bucket_file(b["id"])
post = fm.load(fpath)
idx = int(b["content"].split("#")[1]) - 1 # 0, 1, 2
ts = (datetime.now() - timedelta(hours=(3 - idx) * 10)).isoformat()
post["created"] = ts
post["last_active"] = ts
with open(fpath, "w", encoding="utf-8") as f:
f.write(fm.dumps(post))
all_b = await bm.list_all()
feels = [b for b in all_b if b["metadata"].get("type") == "feel"]
feels.sort(key=lambda b: b["metadata"].get("created", ""), reverse=True)
# Feel #3 has the most recent timestamp
assert "Feel #3" in feels[0]["content"]
@pytest.mark.asyncio
async def test_source_bucket_marked_digested(self, isolated_tools):
"""hold(feel=True, source_bucket=X) marks X as digested."""
bm, dh, de, bd = isolated_tools
# Create a normal bucket first
source_id = await bm.create(
content="和朋友吵了一架",
tags=["社交"], importance=7, domain=["社交"],
valence=0.3, arousal=0.7,
name="争吵", bucket_type="dynamic",
)
# Verify not digested yet
all_b = await bm.list_all()
source = next(b for b in all_b if b["id"] == source_id)
assert not source["metadata"].get("digested", False)
# Create feel referencing it
await bm.create(
content="那次争吵让我意识到沟通的重要性",
tags=[], importance=5, domain=[],
valence=0.5, arousal=0.4,
name=None, bucket_type="feel",
)
# Manually mark digested (simulating server.py hold logic)
await bm.update(source_id, digested=True)
# Verify digested
all_b = await bm.list_all()
source = next(b for b in all_b if b["id"] == source_id)
assert source["metadata"].get("digested") is True
@pytest.mark.asyncio
async def test_feel_never_decays(self, isolated_tools):
"""Feel buckets always score 50.0."""
bm, dh, de, bd = isolated_tools
bid = await bm.create(
content="这是一条永不衰减的 feel",
tags=[], importance=5, domain=[],
valence=0.5, arousal=0.3,
name=None, bucket_type="feel",
)
all_b = await bm.list_all()
feel_b = next(b for b in all_b if b["id"] == bid)
score = de.calculate_score(feel_b["metadata"])
assert score == 50.0
@pytest.mark.asyncio
async def test_feel_not_in_search_merge(self, isolated_tools):
"""Feel buckets excluded from search merge candidates."""
bm, dh, de, bd = isolated_tools
# Create a feel
await bm.create(
content="我对编程的热爱",
tags=[], importance=5, domain=[],
valence=0.8, arousal=0.5,
name=None, bucket_type="feel",
)
# Search should still work but feel shouldn't interfere with merging
results = await bm.search("编程", limit=10)
for r in results:
# Feel buckets may appear in search but shouldn't be merge targets
# (merge logic in server.py checks pinned/protected/feel)
pass # This is a structural test, just verify no crash
@pytest.mark.asyncio
async def test_trace_can_modify_feel(self, isolated_tools):
"""trace() can update feel bucket metadata."""
bm, dh, de, bd = isolated_tools
bid = await bm.create(
content="原始 feel 内容",
tags=[], importance=5, domain=[],
valence=0.5, arousal=0.3,
name=None, bucket_type="feel",
)
# Update content
await bm.update(bid, content="修改后的 feel 内容")
all_b = await bm.list_all()
updated = next(b for b in all_b if b["id"] == bid)
assert "修改后" in updated["content"]
@pytest.mark.asyncio
async def test_feel_crystallization_data(self, isolated_tools):
"""Multiple similar feels exist for crystallization detection."""
bm, dh, de, bd = isolated_tools
# Create 3+ similar feels (about trust)
for i in range(4):
await bm.create(
content=f"P酱对我的信任让我感到温暖每次对话都是一种确认 #{i}",
tags=[], importance=5, domain=[],
valence=0.8, arousal=0.4,
name=None, bucket_type="feel",
)
all_b = await bm.list_all()
feels = [b for b in all_b if b["metadata"].get("type") == "feel"]
assert len(feels) >= 4 # enough for crystallization detection