|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
import pytest
|
|
|
|
|
|
from data_modules.config import DataModulesConfig
|
|
|
-from data_modules.memory.schema import MemoryItem, ScratchpadData
|
|
|
+from data_modules.memory.schema import MemoryItem, ScratchpadData, memory_item_key
|
|
|
from data_modules.memory.store import ScratchpadManager
|
|
|
from data_modules.memory.compactor import compact_scratchpad, _key_for, _is_resolved_open_loop
|
|
|
from data_modules.cli_args import normalize_global_project_root, load_json_arg, _extract_flag_value
|
|
|
@@ -144,8 +144,9 @@ def test_compactor_cleans_resolved_open_loops():
|
|
|
|
|
|
|
|
|
def test_compactor_replaces_existing_timeline_summary():
|
|
|
+ """步骤3的timeline summary只保留一条,即使field值随章节变化。"""
|
|
|
data = ScratchpadData.empty()
|
|
|
- # pre-existing summary
|
|
|
+ # pre-existing summary with old field value
|
|
|
data.story_facts = [
|
|
|
_make_item("sf-old", category="story_fact", subject="timeline_summary", field="<=ch5", value="旧摘要"),
|
|
|
]
|
|
|
@@ -154,15 +155,51 @@ def test_compactor_replaces_existing_timeline_summary():
|
|
|
data.timeline.append(_make_item(f"t-old-{i}", category="timeline", subject=f"旧事件{i}", field="event", value=f"旧事件{i}", chapter=i+1))
|
|
|
# fresh timeline
|
|
|
data.timeline.append(_make_item("t-fresh", category="timeline", subject="新事件", field="event", value="新事件", chapter=60))
|
|
|
- # pad to exceed max
|
|
|
- for i in range(5):
|
|
|
- data.world_rules.append(_make_item(f"wr{i}", category="world_rule", subject=f"rule{i}", field=f"f{i}", value=f"v{i}", chapter=60))
|
|
|
|
|
|
- result = compact_scratchpad(data, max_items=6)
|
|
|
+ # max_items 设足够大,让 step 4 不截断,只测 step 3 的替换逻辑
|
|
|
+ result = compact_scratchpad(data, max_items=4)
|
|
|
summaries = [r for r in result.story_facts if r.subject == "timeline_summary"]
|
|
|
- assert len(summaries) <= 1
|
|
|
- if summaries:
|
|
|
- assert "旧事件" in summaries[0].value
|
|
|
+ # 旧 summary (field="<=ch5") 应被新 summary (field="<=ch3") 替换,而非共存
|
|
|
+ assert len(summaries) == 1
|
|
|
+ assert "旧事件" in summaries[0].value
|
|
|
+ assert summaries[0].field != "<=ch5" # 旧field已被覆盖
|
|
|
+
|
|
|
+
|
|
|
+def test_compactor_resolved_open_loop_integration(tmp_path):
|
|
|
+ """集成测试: compactor通过store.save()触发时正确清除resolved open_loop。"""
|
|
|
+ cfg = _cfg(tmp_path)
|
|
|
+ cfg.memory_compactor_enabled = True
|
|
|
+ cfg.memory_compactor_threshold = 3
|
|
|
+ manager = ScratchpadManager(cfg)
|
|
|
+
|
|
|
+ # 插入一个resolved open_loop
|
|
|
+ manager.upsert_item(_make_item("ol-resolved", category="open_loop", subject="伏笔已解",
|
|
|
+ field="status", value="已解伏笔", payload={"status": "resolved"}))
|
|
|
+ # 插入一个active open_loop
|
|
|
+ manager.upsert_item(_make_item("ol-active", category="open_loop", subject="伏笔未解",
|
|
|
+ field="status", value="未解伏笔", payload={"status": "active"}))
|
|
|
+ # 再插入几条让总数超过threshold触发compaction
|
|
|
+ manager.upsert_item(_make_item("w1", category="world_rule", subject="r1", field="f1", value="v1"))
|
|
|
+ manager.upsert_item(_make_item("w2", category="world_rule", subject="r2", field="f2", value="v2"))
|
|
|
+
|
|
|
+ data = manager.load()
|
|
|
+ loop_subjects = [r.subject for r in data.open_loops]
|
|
|
+ assert "伏笔已解" not in loop_subjects
|
|
|
+ assert "伏笔未解" in loop_subjects
|
|
|
+
|
|
|
+
|
|
|
+def test_memory_item_key_shared_function():
|
|
|
+ """验证 schema.memory_item_key 与 compactor._key_for / store._key_for 一致。"""
|
|
|
+ item = _make_item("x1", category="character_state", subject="hero", field="realm")
|
|
|
+ assert memory_item_key(item) == ("hero", "realm")
|
|
|
+ assert _key_for(item) == memory_item_key(item)
|
|
|
+
|
|
|
+ mgr_key = ScratchpadManager.__new__(ScratchpadManager)
|
|
|
+ # _key_for is instance method, call directly
|
|
|
+ assert ScratchpadManager._key_for(mgr_key, item) == memory_item_key(item)
|
|
|
+
|
|
|
+ unknown = _make_item("u1", category="unknown_category")
|
|
|
+ assert memory_item_key(unknown) == ("u1",)
|
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|