M3a:息壤三级记忆域——归真触发提取管线/白名单明忆/重忆确认/正影版本链/删除墓碑/双通道召回注入,退役旧记忆-抽取-反思-大周天-演化管线(迁移 004,验收 7 用例)
This commit is contained in:
1 parent
2a640a901b
commit
9c98c2f85e
69 files changed
+1193
-6033
No files matched your search
@@ -1,64 +0,0 @@
|
||||
"""大周天循环骨架测试(触发 + 阶段化执行)。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.big_cycle import drain_pending_big_cycles, maybe_trigger_big_cycle
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.memory import Memory
|
||||
from app.repositories import big_cycle as big_cycle_repo
|
||||
from app.services.characters import get_active_character_model
|
||||
|
||||
|
||||
async def _seed_memories(character_id: str, count: int) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for i in range(count):
|
||||
session.add(
|
||||
Memory(
|
||||
character_id=character_id,
|
||||
type="user",
|
||||
title=f"记忆{i}",
|
||||
content=f"内容{i}",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_runs_when_threshold_met() -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert cycle_id is not None
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = await big_cycle_repo.get_cycle(session, cycle_id)
|
||||
assert cycle is not None
|
||||
assert cycle.status == "completed"
|
||||
assert cycle.memory_count_at_trigger >= 20
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_skips_below_threshold() -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
# 默认人物 0 记忆,低于阈值
|
||||
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert cycle_id is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_respects_min_interval() -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
first = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert first is not None
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
# 间隔窗口内再次触发 → 跳过
|
||||
second = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert second is None
|
||||
@@ -1,118 +0,0 @@
|
||||
"""大周天内驱力生成测试(_phase_evaluation 子步骤 _generate_drive)。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.big_cycle import drain_pending_big_cycles, maybe_trigger_big_cycle
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.memory import Memory
|
||||
from app.repositories import drive as drive_repo
|
||||
from app.services.characters import get_active_character_model
|
||||
|
||||
_DRIVE_RESPONSE = (
|
||||
'[{"question":"用户为何频繁提到项目X","source":"memory_gap",'
|
||||
'"expected_value":0.7,"urgency":0.5,"risk_level":"L2",'
|
||||
'"related_entities":["项目X"],'
|
||||
'"subgoals":[{"title":"询问项目X目标","description":"主动询问用户",'
|
||||
'"expected_information_gain":0.6,"interruption_cost":0.3,"risk_level":"L4"}]}]'
|
||||
)
|
||||
|
||||
|
||||
async def _seed_memories(character_id: str, count: int) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for i in range(count):
|
||||
session.add(
|
||||
Memory(
|
||||
character_id=character_id,
|
||||
type="user",
|
||||
title=f"记忆{i}",
|
||||
content=f"内容{i}",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_drive_chat(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def fake_chat(*_args, **_kwargs):
|
||||
return _DRIVE_RESPONSE
|
||||
|
||||
monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_generates_curiosity_and_subgoal(
|
||||
mock_drive_chat, # noqa: ARG001 - pytest fixture 靠 monkeypatch 副作用生效
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert cycle_id is not None
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
curiosities, c_total = await drive_repo.list_curiosities(session, character.id)
|
||||
subgoals, s_total = await drive_repo.list_subgoals(session, character.id)
|
||||
|
||||
assert c_total == 1
|
||||
assert curiosities[0].question == "用户为何频繁提到项目X"
|
||||
assert curiosities[0].source == "memory_gap"
|
||||
assert curiosities[0].cycle_id == cycle_id
|
||||
assert s_total == 1
|
||||
assert subgoals[0].status == "candidate"
|
||||
assert subgoals[0].curiosity_question_id == curiosities[0].id
|
||||
assert subgoals[0].title == "询问项目X目标"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_auto_approves_low_risk_subgoal(
|
||||
mock_drive_chat, # noqa: ARG001
|
||||
) -> None:
|
||||
"""低风险次生目标(L0-L2)应在大周天中自动通过并生成 Goal。"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models.governance import Goal
|
||||
|
||||
# 使用低风险的子目标响应
|
||||
_low_risk_response = (
|
||||
'[{"question":"还有哪些未知领域","source":"world_uncertainty",'
|
||||
'"expected_value":0.6,"urgency":0.4,"risk_level":"L1",'
|
||||
'"subgoals":[{"title":"探索未知领域","description":"自动探索",'
|
||||
'"expected_information_gain":0.5,"interruption_cost":0.2,"risk_level":"L1"}]}]'
|
||||
)
|
||||
|
||||
async def fake_low_risk(*_args, **_kwargs):
|
||||
return _low_risk_response
|
||||
|
||||
import app.application.big_cycle as bc
|
||||
|
||||
original = bc._call_model
|
||||
bc._call_model = fake_low_risk
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert cycle_id is not None
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
bc._call_model = original
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
subgoals, s_total = await drive_repo.list_subgoals(session, character.id)
|
||||
assert s_total == 1
|
||||
assert subgoals[0].status in ("approved", "auto_execute_pending"), (
|
||||
f"预期 approved 或 auto_execute_pending,实际={subgoals[0].status}"
|
||||
)
|
||||
|
||||
# 验证自动生成了 Goal
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(Goal).where(Goal.character_id == character.id)
|
||||
)
|
||||
goals = result.scalars().all()
|
||||
matching = [g for g in goals if g.title == "探索未知领域"]
|
||||
assert len(matching) == 1
|
||||
assert matching[0].level == "task"
|
||||
@@ -1,142 +0,0 @@
|
||||
"""大周天候选生成 + 半自动应用调度测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.big_cycle import drain_pending_big_cycles, maybe_trigger_big_cycle
|
||||
from app.core.config import settings
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.memory import Memory
|
||||
from app.repositories import big_cycle as big_cycle_repo
|
||||
from app.repositories import evolution as evolution_repo
|
||||
from app.services.characters import get_active_character_model
|
||||
|
||||
_MIXED_RESPONSE = (
|
||||
"["
|
||||
'{"type":"personality","level":"L4","scope":"character_scope",'
|
||||
'"title":"身份演化","proposal":"调整身份表达",'
|
||||
'"delta":{"identity_profile":"新的身份描述"},'
|
||||
'"confidence":0.7,"benefit_score":0.6,"risk_score":0.4,'
|
||||
'"auto_apply_allowed":false,"evidence":[0]},'
|
||||
'{"type":"procedural","level":"L2","scope":"task_scope",'
|
||||
'"title":"确认假设","proposal":"技术问题先确认前提",'
|
||||
'"delta":{"title":"确认假设","content":"先确认前提再作答"},'
|
||||
'"confidence":0.8,"benefit_score":0.7,"risk_score":0.2,'
|
||||
'"auto_apply_allowed":true,"evidence":[0]}'
|
||||
"]"
|
||||
)
|
||||
|
||||
|
||||
async def _seed_memories(character_id: str, count: int) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for i in range(count):
|
||||
session.add(
|
||||
Memory(
|
||||
character_id=character_id,
|
||||
type="user",
|
||||
title=f"记忆{i}",
|
||||
content=f"内容{i}",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_evolution_chat(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def fake_chat(*_args, **_kwargs):
|
||||
return _MIXED_RESPONSE
|
||||
|
||||
monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_generates_candidates_and_auto_applies(
|
||||
mock_evolution_chat, # noqa: ARG001 - pytest fixture 靠 monkeypatch 副作用生效
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert cycle_id is not None
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = await big_cycle_repo.get_cycle(session, cycle_id)
|
||||
candidates, total = await evolution_repo.list_candidates(session, character.id)
|
||||
|
||||
assert cycle is not None
|
||||
assert cycle.status == "completed"
|
||||
assert cycle.evaluation_snapshot_json is not None
|
||||
assert total == 2
|
||||
|
||||
personality = next(c for c in candidates if c.type == "personality")
|
||||
assert personality.status == "pending" # L4 auto_apply_allowed=false
|
||||
assert personality.level == "L4"
|
||||
assert personality.source == "big_cycle"
|
||||
assert personality.cycle_id == cycle_id
|
||||
|
||||
procedural = next(c for c in candidates if c.type == "procedural")
|
||||
assert procedural.status == "applied" # L2 auto_apply_allowed=true
|
||||
assert procedural.resulting_target_id is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_apply_degrades_when_applier_missing(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
response = (
|
||||
'[{"type":"tool_strategy","level":"L2","scope":"task_scope",'
|
||||
'"title":"工具策略","proposal":"偏好shell",'
|
||||
'"delta":{},"auto_apply_allowed":true}]'
|
||||
)
|
||||
|
||||
async def fake_chat(*_args, **_kwargs):
|
||||
return response
|
||||
|
||||
monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
candidates, _ = await evolution_repo.list_candidates(session, character.id)
|
||||
assert len(candidates) == 1
|
||||
cand = candidates[0]
|
||||
assert cand.status == "pending" # applier 缺失,降级不自动应用
|
||||
assert cand.auto_apply_allowed is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_uses_configured_model_hint(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict = {}
|
||||
result = type("FakeResponse", (), {"content": "[]"})()
|
||||
|
||||
class _FakeManager:
|
||||
def to_message_list(self, *_args: object, **_kwargs: object) -> list:
|
||||
return []
|
||||
|
||||
async def chat(self, messages: object, model_hint: str | None = None) -> object: # noqa: ARG002
|
||||
captured["model_hint"] = model_hint
|
||||
return result
|
||||
|
||||
def fake_get_provider_manager() -> _FakeManager:
|
||||
return _FakeManager()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.application.big_cycle._get_provider_manager", fake_get_provider_manager
|
||||
)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
assert captured["model_hint"] == settings.big_cycle_model
|
||||
@@ -1,76 +0,0 @@
|
||||
"""大周天记忆/世界抽取生成测试(_phase_memory 阶段)。
|
||||
|
||||
高置信度(≥0.7)低敏感度记忆候选自动落地到 memories 表;
|
||||
世界实体候选(confidence=0.6)低于阈值仍留 pending。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.big_cycle import drain_pending_big_cycles, maybe_trigger_big_cycle
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.memory import Memory
|
||||
from app.repositories import world as world_repo
|
||||
from app.services.characters import get_active_character_model
|
||||
|
||||
_EXTRACTION_RESPONSE = (
|
||||
'{"memories":[{"type":"preference","title":"喜欢简洁","content":"用户偏好简洁",'
|
||||
'"importance":0.8,"sensitivity":"low","confidence":0.7}],'
|
||||
'"entities":[{"type":"project","name":"项目X","attributes":{"status":"active"},'
|
||||
'"state_type":"inference","confidence":0.6}]}'
|
||||
)
|
||||
|
||||
|
||||
async def _seed_memories(character_id: str, count: int) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
for i in range(count):
|
||||
session.add(
|
||||
Memory(
|
||||
character_id=character_id,
|
||||
type="user",
|
||||
title=f"记忆{i}",
|
||||
content=f"内容{i}",
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_extraction_chat(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def fake_chat(*_args, **_kwargs):
|
||||
return _EXTRACTION_RESPONSE
|
||||
|
||||
monkeypatch.setattr("app.application.big_cycle._call_model", fake_chat)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_big_cycle_extracts_memory_and_world(
|
||||
mock_extraction_chat, # noqa: ARG001 - pytest fixture 靠 monkeypatch 副作用生效
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
await _seed_memories(character.id, 20)
|
||||
|
||||
cycle_id = await maybe_trigger_big_cycle(character.id, trigger="test")
|
||||
assert cycle_id is not None
|
||||
await drain_pending_big_cycles()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 高置信度(≥0.7)低敏感度记忆自动落地到 memories 表
|
||||
from sqlalchemy import select
|
||||
|
||||
result = await session.execute(
|
||||
select(Memory).where(
|
||||
Memory.character_id == character.id,
|
||||
Memory.source == "big_cycle",
|
||||
)
|
||||
)
|
||||
auto_memories = result.scalars().all()
|
||||
# 世界实体候选(confidence=0.6 < 0.7)仍留 pending
|
||||
entities, _ = await world_repo.list_entities(session, character.id, status="pending")
|
||||
|
||||
assert len(auto_memories) == 1
|
||||
assert auto_memories[0].title == "喜欢简洁"
|
||||
assert len(entities) == 1
|
||||
assert entities[0].name == "项目X"
|
||||
assert entities[0].status == "pending"
|
||||
assert entities[0].state_type == "inference"
|
||||
@@ -1,171 +0,0 @@
|
||||
"""大周天中的目标进度更新测试。
|
||||
|
||||
验证 _phase_goal_progress 在独立环境中按预期工作:
|
||||
- 活跃的 task/session 目标进度 +0.1
|
||||
- 进度上限 1.0
|
||||
- 非活跃目标不受影响
|
||||
- 进度达 1.0 的 task 目标状态标记为 completed
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.big_cycle import _phase_goal_progress
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.big_cycle import BigCycle
|
||||
from app.models.governance import Goal
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_progress_increments_active_tasks() -> None:
|
||||
"""task 级别活跃目标在大周天后进度增加 0.1。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = BigCycle(character_id="char_pg1", trigger="test")
|
||||
session.add(cycle)
|
||||
goal = Goal(
|
||||
character_id="char_pg1",
|
||||
title="测试任务",
|
||||
level="task",
|
||||
status="active",
|
||||
progress=0.3,
|
||||
)
|
||||
session.add(goal)
|
||||
await session.flush()
|
||||
|
||||
await _phase_goal_progress(session, cycle)
|
||||
|
||||
assert goal.progress == 0.4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_progress_caps_at_one() -> None:
|
||||
"""进度达到 1.0 后不再增加。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = BigCycle(character_id="char_pg2", trigger="test")
|
||||
session.add(cycle)
|
||||
goal = Goal(
|
||||
character_id="char_pg2",
|
||||
title="已满进度",
|
||||
level="task",
|
||||
status="active",
|
||||
progress=1.0,
|
||||
)
|
||||
session.add(goal)
|
||||
await session.flush()
|
||||
|
||||
await _phase_goal_progress(session, cycle)
|
||||
|
||||
# 已达上限,不再增加
|
||||
assert goal.progress == 1.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_progress_skips_non_active() -> None:
|
||||
"""非活跃目标不被更新。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = BigCycle(character_id="char_pg3", trigger="test")
|
||||
session.add(cycle)
|
||||
goal = Goal(
|
||||
character_id="char_pg3",
|
||||
title="已完成任务",
|
||||
level="task",
|
||||
status="completed",
|
||||
progress=0.5,
|
||||
)
|
||||
session.add(goal)
|
||||
await session.flush()
|
||||
|
||||
await _phase_goal_progress(session, cycle)
|
||||
|
||||
assert goal.progress == 0.5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_progress_skips_highest_level() -> None:
|
||||
"""highest 级别目标不被大周天自动更新。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = BigCycle(character_id="char_pg4", trigger="test")
|
||||
session.add(cycle)
|
||||
goal = Goal(
|
||||
character_id="char_pg4",
|
||||
title="最高使命",
|
||||
level="highest",
|
||||
type="mission",
|
||||
status="active",
|
||||
progress=0.5,
|
||||
)
|
||||
session.add(goal)
|
||||
await session.flush()
|
||||
|
||||
await _phase_goal_progress(session, cycle)
|
||||
|
||||
assert goal.progress == 0.5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_progress_only_updates_own_character() -> None:
|
||||
"""仅更新当前 character 的目标,不影响其他角色。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = BigCycle(character_id="char_pg5", trigger="test")
|
||||
session.add(cycle)
|
||||
goal_own = Goal(
|
||||
character_id="char_pg5",
|
||||
title="自己任务",
|
||||
level="task",
|
||||
status="active",
|
||||
progress=0.0,
|
||||
)
|
||||
goal_other = Goal(
|
||||
character_id="char_other",
|
||||
title="他人任务",
|
||||
level="task",
|
||||
status="active",
|
||||
progress=0.0,
|
||||
)
|
||||
session.add_all([goal_own, goal_other])
|
||||
await session.flush()
|
||||
|
||||
await _phase_goal_progress(session, cycle)
|
||||
|
||||
assert goal_own.progress == 0.1
|
||||
assert goal_other.progress == 0.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_progress_completes_task_at_one() -> None:
|
||||
"""task 级别目标进度达到 1.0 后状态变为 completed。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle = BigCycle(character_id="char_pg6", trigger="test")
|
||||
session.add(cycle)
|
||||
goal = Goal(
|
||||
character_id="char_pg6",
|
||||
title="即将完成",
|
||||
level="task",
|
||||
status="active",
|
||||
progress=0.95,
|
||||
)
|
||||
session.add(goal)
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
cycle_id = cycle.id
|
||||
goal_id = goal.id
|
||||
|
||||
# 清理可能被前序测试设置的 event_store,避免 emit_goal_event 的独立 session 冲突
|
||||
from app.main import app as _app
|
||||
|
||||
_app.state.__dict__.pop("event_store", None)
|
||||
|
||||
# 关闭原 session 后调用 _phase_goal_progress,避免 emit_goal_event
|
||||
# 在 SQLite 单文件模式下产生写锁冲突时,progress 更新仍应正确提交。
|
||||
async with AsyncSessionLocal() as session:
|
||||
cycle_ref = await session.get(BigCycle, cycle_id)
|
||||
try: # noqa: SIM105 — emit_goal_event 失败不影响 goal 更新
|
||||
await _phase_goal_progress(session, cycle_ref)
|
||||
except Exception:
|
||||
pass
|
||||
await session.commit()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
goal_ref = await session.get(Goal, goal_id)
|
||||
assert goal_ref is not None
|
||||
assert goal_ref.progress == 1.0
|
||||
assert goal_ref.status == "completed"
|
||||
@@ -1,100 +0,0 @@
|
||||
"""小周天每轮记忆候选 + 反思抽取测试。
|
||||
|
||||
高置信度(≥0.7)低敏感度记忆候选直接落地到 memories 表而非 MemoryCandidate。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.application.reflection import (
|
||||
drain_pending_reflections,
|
||||
start_reflection_after_turn,
|
||||
)
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.memory import Memory
|
||||
from app.repositories import candidates as candidates_repo
|
||||
from app.services.characters import get_active_character_model
|
||||
|
||||
_REFLECTION_RESPONSE = (
|
||||
'{"memories":[{"type":"preference","title":"喜欢简洁","content":"用户偏好简洁",'
|
||||
'"importance":0.8,"sensitivity":"low","confidence":0.7}],'
|
||||
'"reflection":"本轮应更主动确认用户意图。"}'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_reflection_chat(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def fake_chat(*_args, **_kwargs):
|
||||
return _REFLECTION_RESPONSE
|
||||
|
||||
monkeypatch.setattr("app.application.reflection._call_model", fake_chat)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reflection_extracts_memory_and_writes_reflection(
|
||||
mock_reflection_chat, # noqa: ARG001
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
|
||||
start_reflection_after_turn(character.id, "session_test", "你好", "你好,我是魂镜。")
|
||||
await drain_pending_reflections()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 高置信度记忆直接落地到 memories 表(非 MemoryCandidate)
|
||||
auto_mem = (
|
||||
await session.execute(
|
||||
select(Memory).where(
|
||||
Memory.character_id == character.id,
|
||||
Memory.type == "preference",
|
||||
Memory.source == "reflection",
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
# 反思文本也落地为 memories 表
|
||||
ref_mem = (
|
||||
await session.execute(
|
||||
select(Memory).where(
|
||||
Memory.character_id == character.id,
|
||||
Memory.type == "reflection",
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
assert auto_mem is not None
|
||||
assert auto_mem.title == "喜欢简洁"
|
||||
assert ref_mem is not None
|
||||
assert ref_mem.source == "reflection"
|
||||
assert ref_mem.status == "active" # Memory 无 status 字段,用 default
|
||||
assert "确认用户意图" in ref_mem.content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reflection_without_memories_only_writes_reflection(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def fake_chat(*_args, **_kwargs):
|
||||
return '{"memories":[],"reflection":"只反思不抽记忆。"}'
|
||||
|
||||
monkeypatch.setattr("app.application.reflection._call_model", fake_chat)
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session)
|
||||
|
||||
start_reflection_after_turn(character.id, "session_test", "hi", "你好。")
|
||||
await drain_pending_reflections()
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
cands = await candidates_repo.list_memory_candidates(session, character.id, source="cycle")
|
||||
ref_mem = (
|
||||
await session.execute(
|
||||
select(Memory).where(
|
||||
Memory.character_id == character.id,
|
||||
Memory.type == "reflection",
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
# 无记忆候选(空 memories 数组),只有反思
|
||||
assert len(cands) == 0
|
||||
assert ref_mem is not None
|
||||
assert "只反思" in ref_mem.content
|
||||
Reference in new issue
Block a user