Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

316 lines
12 KiB
Python

"""End-to-end (LLM-mocked) tests for the three modes.
These are the load-bearing tests for the new pipeline. The LLM call is
mocked at the ``call_llm`` boundary in :mod:`modes._runtime`; everything
else (chunker, ref validation, doc IO, meta) runs for real on a temp
memory dir.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from deeptutor.services.memory import paths as paths_mod
from deeptutor.services.memory.consolidator.modes import audit as audit_mod
from deeptutor.services.memory.consolidator.modes import dedup as dedup_mod
from deeptutor.services.memory.consolidator.modes import update as update_mod
from deeptutor.services.memory.document import Document, Entry, parse, serialize
from deeptutor.services.memory.ids import new_entry_id
from deeptutor.services.memory.snapshot.entity import Entity
@pytest.fixture()
def memory_dir(tmp_path: Path, monkeypatch):
monkeypatch.setattr(paths_mod, "memory_root", lambda: tmp_path)
(tmp_path / "L2").mkdir(parents=True, exist_ok=True)
(tmp_path / "L3").mkdir(parents=True, exist_ok=True)
(tmp_path / "trace").mkdir(parents=True, exist_ok=True)
yield tmp_path
def _entity(eid: str, content: str = "user uses spaced repetition with FSRS scheduler.") -> Entity:
return Entity(
id=eid,
label=f"entry {eid}",
ts="2026-05-19T00:00:00Z",
content=content,
metadata={},
fingerprint="fp",
)
# ── update — L2 ─────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_update_l2_appends_facts_from_chunk(memory_dir, monkeypatch):
entities = [_entity("01ABC"), _entity("01DEF")]
monkeypatch.setattr(
"deeptutor.services.memory.consolidator.modes.update.snap.read_snapshot",
lambda surface: entities,
)
async def fake_llm(*, system_prompt, user_prompt, **kwargs):
# Return one valid fact per call, citing a ref in the chunk.
if "01ABC" in user_prompt:
return '{"facts": [{"text": "uses FSRS scheduling", "section": "Mastery", "refs": ["chat:01ABC"]}]}'
if "01DEF" in user_prompt:
return '{"facts": [{"text": "scheduler customisation", "section": "Mastery", "refs": ["chat:01DEF"]}]}'
return '{"facts": []}'
# Force a tiny chunker so each entity ends up in its own chunk.
with (
patch("deeptutor.services.memory.consolidator.modes.update.call_llm", side_effect=fake_llm),
patch.object(update_mod, "load_memory_settings") as mock_settings,
):
from deeptutor.services.memory.settings import (
ChunkingSettings,
DedupSettings,
MemorySettings,
)
mock_settings.return_value = MemorySettings(
chunking=ChunkingSettings(min_chunk_chars=200, max_chunk_chars=400, overlap_ratio=0.0),
dedup=DedupSettings(auto_after_update=False),
)
result = await update_mod.run_update("L2", "chat", language="en")
assert result.facts_added >= 1
assert not result.no_new_input
md = (memory_dir / "L2" / "chat.md").read_text(encoding="utf-8")
assert "## Mastery" in md
assert "FSRS" in md or "scheduler" in md
@pytest.mark.asyncio
async def test_update_l2_idempotent_when_no_new_entities(memory_dir, monkeypatch):
entities = [_entity("01ABC")]
monkeypatch.setattr(
"deeptutor.services.memory.consolidator.modes.update.snap.read_snapshot",
lambda surface: entities,
)
# First run records the entity in meta.
async def llm_returns_one(*, system_prompt, user_prompt, **kwargs):
return '{"facts": [{"text": "uses Anki", "section": "Topics", "refs": ["chat:01ABC"]}]}'
with (
patch(
"deeptutor.services.memory.consolidator.modes.update.call_llm",
side_effect=llm_returns_one,
),
patch.object(update_mod, "load_memory_settings") as mock_settings,
):
from deeptutor.services.memory.settings import DedupSettings, MemorySettings
mock_settings.return_value = MemorySettings(dedup=DedupSettings(auto_after_update=False))
first = await update_mod.run_update("L2", "chat", language="en")
assert first.facts_added >= 0
# Second run with the same entities: no new traces → no LLM calls,
# no facts added.
llm_called = []
async def llm_should_not_run(*args, **kwargs):
llm_called.append(1)
return '{"facts": []}'
with (
patch(
"deeptutor.services.memory.consolidator.modes.update.call_llm",
side_effect=llm_should_not_run,
),
patch.object(update_mod, "load_memory_settings") as mock_settings,
):
from deeptutor.services.memory.settings import DedupSettings, MemorySettings
mock_settings.return_value = MemorySettings(dedup=DedupSettings(auto_after_update=False))
second = await update_mod.run_update("L2", "chat", language="en")
assert second.no_new_input is True
assert llm_called == []
@pytest.mark.asyncio
async def test_update_l2_drops_facts_with_out_of_pool_refs(memory_dir, monkeypatch):
entities = [_entity("01ABC")]
monkeypatch.setattr(
"deeptutor.services.memory.consolidator.modes.update.snap.read_snapshot",
lambda surface: entities,
)
async def fake_llm(*, system_prompt, user_prompt, **kwargs):
# Return one fact with a ref not in the chunk pool.
return (
'{"facts": [{"text": "uses Anki", "section": "Topics", "refs": ["chat:NOT_IN_CHUNK"]}]}'
)
with (
patch("deeptutor.services.memory.consolidator.modes.update.call_llm", side_effect=fake_llm),
patch.object(update_mod, "load_memory_settings") as mock_settings,
):
from deeptutor.services.memory.settings import (
DedupSettings,
MemorySettings,
ReferenceSettings,
)
mock_settings.return_value = MemorySettings(
dedup=DedupSettings(auto_after_update=False),
reference=ReferenceSettings(enforce_required=True, drop_invalid_refs=True),
)
result = await update_mod.run_update("L2", "chat", language="en")
# The fact had only an out-of-pool ref → dropped.
assert result.refs_dropped >= 1
assert result.facts_added == 0
# ── audit — L2 ──────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_audit_l2_applies_replace_edit(memory_dir, monkeypatch):
# Seed an existing L2 doc.
ids = [new_entry_id()]
doc = Document(
title="chat memory",
sections=[
("Topics", [Entry(id=ids[0], section="Topics", text="claims X", refs=["chat:01ABC"])])
],
)
path = memory_dir / "L2" / "chat.md"
path.write_text(serialize(doc), encoding="utf-8")
monkeypatch.setattr(
"deeptutor.services.memory.consolidator.modes.audit.snap.read_snapshot",
lambda surface: [_entity("01ABC", content="the user actually said Y, not X")],
)
async def fake_llm(*, system_prompt, user_prompt, **kwargs):
# Find the bullet line and emit a replace.
line_no = None
for ln in user_prompt.splitlines():
if "claims X" in ln and ln.lstrip().startswith(("3", "4", "5", "6", "7", "8")):
line_no = int(ln.strip().split(":")[0])
break
if line_no is None:
return '{"edits": []}'
return (
'{"edits": [{"op": "replace", "line": '
+ str(line_no)
+ ', "new_text": "claims Y", "refs": ["chat:01ABC"], "reason": "matched evidence"}]}'
)
with patch("deeptutor.services.memory.consolidator.modes.audit.call_llm", side_effect=fake_llm):
result = await audit_mod.run_audit("L2", "chat", language="en", budget=1)
new_md = path.read_text(encoding="utf-8")
assert "claims Y" in new_md
assert result.edits_applied >= 1
# ── dedup ───────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_dedup_early_stop_when_no_edits(memory_dir, monkeypatch):
ids = [new_entry_id() for _ in range(2)]
doc = Document(
title="chat memory",
sections=[
(
"Topics",
[
Entry(id=ids[0], section="Topics", text="alpha", refs=["chat:01"]),
Entry(id=ids[1], section="Topics", text="beta", refs=["chat:02"]),
],
)
],
)
path = memory_dir / "L2" / "chat.md"
path.write_text(serialize(doc), encoding="utf-8")
llm_calls = []
async def fake_llm(*, system_prompt, user_prompt, **kwargs):
llm_calls.append(1)
return '{"edits": []}'
with (
patch("deeptutor.services.memory.consolidator.modes.dedup.call_llm", side_effect=fake_llm),
patch.object(dedup_mod, "load_memory_settings") as mock_settings,
):
from deeptutor.services.memory.settings import DedupSettings, MemorySettings
mock_settings.return_value = MemorySettings(
dedup=DedupSettings(iterations=5, auto_after_update=False)
)
result = await dedup_mod.run_dedup("L2", "chat", language="en")
assert result.converged_early is True
assert result.iterations_run == 1
assert len(llm_calls) == 1
@pytest.mark.asyncio
async def test_dedup_applies_delete_then_stops(memory_dir, monkeypatch):
ids = [new_entry_id() for _ in range(2)]
doc = Document(
title="chat memory",
sections=[
(
"Topics",
[
Entry(id=ids[0], section="Topics", text="duplicate fact", refs=["chat:01"]),
Entry(id=ids[1], section="Topics", text="duplicate fact", refs=["chat:02"]),
],
)
],
)
path = memory_dir / "L2" / "chat.md"
path.write_text(serialize(doc), encoding="utf-8")
call_count = [0]
async def fake_llm(*, system_prompt, user_prompt, **kwargs):
call_count[0] += 1
if call_count[0] == 1:
# Find the second bullet's line number.
line_no = None
seen = 0
for ln in user_prompt.splitlines():
if "duplicate fact" in ln and ln.lstrip()[:2].rstrip(":").isdigit():
seen += 1
if seen == 2:
line_no = int(ln.strip().split(":")[0])
break
if line_no is None:
return '{"edits": []}'
return (
'{"edits": [{"op": "delete", "line_start": '
+ str(line_no)
+ ', "line_end": '
+ str(line_no)
+ ', "reason": "duplicate"}]}'
)
return '{"edits": []}'
with (
patch("deeptutor.services.memory.consolidator.modes.dedup.call_llm", side_effect=fake_llm),
patch.object(dedup_mod, "load_memory_settings") as mock_settings,
):
from deeptutor.services.memory.settings import DedupSettings, MemorySettings
mock_settings.return_value = MemorySettings(
dedup=DedupSettings(iterations=3, auto_after_update=False)
)
result = await dedup_mod.run_dedup("L2", "chat", language="en")
assert result.edits_applied >= 1
new_doc = parse(path.read_text(encoding="utf-8"))
assert len([e for e in new_doc.all_entries() if e.text == "duplicate fact"]) == 1