mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-01 16:51:53 +03:00
refactor(memory): unify structured consolidation flow
This commit is contained in:
@@ -7,7 +7,6 @@ import pytest
|
||||
|
||||
from nanobot.agent.autocompact import AutoCompact
|
||||
from nanobot.session.manager import Session, SessionManager
|
||||
from nanobot.session.summary import SessionSummary
|
||||
|
||||
|
||||
def _runtime(_session: Session | None = None):
|
||||
@@ -176,24 +175,6 @@ class TestIsExpired:
|
||||
assert ac._is_expired(expired, now=now) is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SessionSummary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSessionSummary:
|
||||
"""Test prompt rendering for the structured summary value."""
|
||||
|
||||
def test_formats_prompt(self):
|
||||
last_active = datetime(2026, 5, 13, 14, 30, 0)
|
||||
summary = SessionSummary("User discussed Python.", last_active)
|
||||
|
||||
assert summary.for_prompt() == (
|
||||
"Previous conversation summary (last active 2026-05-13T14:30:00):\n"
|
||||
"User discussed Python."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check_expired
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -490,7 +471,7 @@ class TestArchiveDelegates:
|
||||
|
||||
entry = ac._summaries.get("cli:test")
|
||||
assert entry is not None
|
||||
assert entry.text == "Hello."
|
||||
assert entry["text"] == "Hello."
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_summary_when_compact_returns_empty(self):
|
||||
@@ -569,21 +550,29 @@ class TestPrepareSession:
|
||||
ac = _make_autocompact()
|
||||
session = _make_session()
|
||||
last_active = datetime(2026, 5, 13, 14, 0, 0)
|
||||
ac._summaries["cli:test"] = SessionSummary("Hot summary.", last_active)
|
||||
ac._summaries["cli:test"] = {
|
||||
"text": "Hot summary.",
|
||||
"last_active": last_active.isoformat(),
|
||||
}
|
||||
|
||||
result_session, summary = ac.prepare_session(session, "cli:test")
|
||||
|
||||
assert result_session is session
|
||||
assert summary is not None
|
||||
assert summary.text == "Hot summary."
|
||||
assert "Previous conversation summary" in summary.for_prompt()
|
||||
assert summary == {
|
||||
"text": "Hot summary.",
|
||||
"last_active": last_active.isoformat(),
|
||||
}
|
||||
|
||||
def test_hot_path_pops_summary_one_shot(self):
|
||||
"""Hot path should pop the summary (one-shot; second call returns None)."""
|
||||
ac = _make_autocompact()
|
||||
session = _make_session()
|
||||
last_active = datetime(2026, 1, 1)
|
||||
ac._summaries["cli:test"] = SessionSummary("One-shot.", last_active)
|
||||
ac._summaries["cli:test"] = {
|
||||
"text": "One-shot.",
|
||||
"last_active": last_active.isoformat(),
|
||||
}
|
||||
|
||||
_, summary1 = ac.prepare_session(session, "cli:test")
|
||||
assert summary1 is not None
|
||||
@@ -606,7 +595,7 @@ class TestPrepareSession:
|
||||
|
||||
assert result_session is session
|
||||
assert summary is not None
|
||||
assert summary.text == "Cold summary."
|
||||
assert summary["text"] == "Cold summary."
|
||||
|
||||
def test_cold_path_tolerates_malformed_last_active(self):
|
||||
"""A malformed persisted last_active must not raise on the turn path.
|
||||
@@ -629,8 +618,10 @@ class TestPrepareSession:
|
||||
|
||||
assert result_session is session
|
||||
assert summary is not None
|
||||
assert summary.text == "Cold summary."
|
||||
assert summary.last_active == fallback
|
||||
assert summary == {
|
||||
"text": "Cold summary.",
|
||||
"last_active": fallback.isoformat(),
|
||||
}
|
||||
|
||||
def test_cold_path_tolerates_missing_last_active(self):
|
||||
"""A _last_summary dict without last_active must not raise."""
|
||||
@@ -645,8 +636,10 @@ class TestPrepareSession:
|
||||
|
||||
assert result_session is session
|
||||
assert summary is not None
|
||||
assert summary.text == "Cold summary."
|
||||
assert summary.last_active == fallback
|
||||
assert summary == {
|
||||
"text": "Cold summary.",
|
||||
"last_active": fallback.isoformat(),
|
||||
}
|
||||
|
||||
def test_cold_path_missing_text_returns_none(self):
|
||||
"""A _last_summary without a non-empty string text yields no summary."""
|
||||
@@ -677,7 +670,10 @@ class TestPrepareSession:
|
||||
ac.sessions = mock_sm
|
||||
key = "dream:20260602-155256"
|
||||
ac._archiving.add(key)
|
||||
ac._summaries[key] = SessionSummary("Hot summary.", datetime(2026, 6, 2, 15, 52, 56))
|
||||
ac._summaries[key] = {
|
||||
"text": "Hot summary.",
|
||||
"last_active": "2026-06-02T15:52:56",
|
||||
}
|
||||
session = _make_session(
|
||||
key=key,
|
||||
updated_at=datetime.now() - timedelta(minutes=20),
|
||||
@@ -717,9 +713,12 @@ class TestPrepareSession:
|
||||
},
|
||||
})
|
||||
last_active = datetime(2026, 5, 13, 14, 0, 0)
|
||||
ac._summaries["cli:test"] = SessionSummary("Hot summary.", last_active)
|
||||
ac._summaries["cli:test"] = {
|
||||
"text": "Hot summary.",
|
||||
"last_active": last_active.isoformat(),
|
||||
}
|
||||
|
||||
_, summary = ac.prepare_session(session, "cli:test")
|
||||
assert summary is not None
|
||||
assert summary.text == "Hot summary."
|
||||
assert summary["text"] == "Hot summary."
|
||||
# After hot path pops, cold path would kick in on next call
|
||||
|
||||
Reference in New Issue
Block a user