mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/memory+cli: session rename, delete, and export
Add MemoryStore rename/delete with explicit message cleanup. Slash commands /rename, /delete (confirm), /export md|json write transcripts from DB; deleting the current session starts a fresh empty one.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from plyngent.cli.export import (
|
||||
encode_session_export_json,
|
||||
format_session_export_md,
|
||||
session_export_payload,
|
||||
)
|
||||
from plyngent.lmproto.openai_compatible.model import AssistantChatMessage, UserChatMessage
|
||||
|
||||
|
||||
def test_format_session_export_md() -> None:
|
||||
text = format_session_export_md(
|
||||
[
|
||||
UserChatMessage(content="hi"),
|
||||
AssistantChatMessage(content="yo", reasoning_content="plan"),
|
||||
],
|
||||
sid=7,
|
||||
name="demo",
|
||||
workspace="/tmp/ws",
|
||||
)
|
||||
assert "# Session 7: demo" in text
|
||||
assert "## user" in text
|
||||
assert "hi" in text
|
||||
assert "### reasoning" in text
|
||||
assert "plan" in text
|
||||
assert "yo" in text
|
||||
|
||||
|
||||
def test_session_export_json_roundtrip() -> None:
|
||||
messages = [UserChatMessage(content="a")]
|
||||
now = datetime.now(UTC)
|
||||
payload = session_export_payload(
|
||||
sid=7,
|
||||
name="demo",
|
||||
workspace="/tmp/ws",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
messages=messages,
|
||||
)
|
||||
assert isinstance(payload, dict)
|
||||
raw = encode_session_export_json(payload)
|
||||
assert "7" in raw
|
||||
assert "demo" in raw
|
||||
@@ -116,6 +116,64 @@ async def test_tools_toggle(state: ReplState) -> None:
|
||||
assert state.tools_enabled is False
|
||||
|
||||
|
||||
async def test_rename_slash(state: ReplState) -> None:
|
||||
sid = state.session_id
|
||||
assert sid is not None
|
||||
assert await handle_slash(state, "/rename my-chat") is True
|
||||
row = await state.memory.get_session(sid)
|
||||
assert row is not None
|
||||
assert row.name == "my-chat"
|
||||
|
||||
|
||||
async def test_delete_slash_confirm(
|
||||
state: ReplState,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
from plyngent.prompting import temporary_backend
|
||||
from tests.test_prompting import ScriptedBackend
|
||||
|
||||
# Delete a non-current session so SQLite cannot reuse the same sid as "current".
|
||||
victim = state.session_id
|
||||
assert victim is not None
|
||||
assert await handle_slash(state, "/new keep") is True
|
||||
current = state.session_id
|
||||
assert current != victim
|
||||
|
||||
with temporary_backend(ScriptedBackend([], confirms=[False])):
|
||||
assert await handle_slash(state, f"/delete {victim}") is True
|
||||
assert await state.memory.get_session(victim) is not None
|
||||
assert "cancelled" in capsys.readouterr().out
|
||||
|
||||
with temporary_backend(ScriptedBackend([], confirms=[True])):
|
||||
assert await handle_slash(state, f"/delete {victim}") is True
|
||||
assert await state.memory.get_session(victim) is None
|
||||
assert state.session_id == current
|
||||
out = capsys.readouterr().out
|
||||
assert "deleted" in out
|
||||
assert "new session" not in out
|
||||
|
||||
|
||||
async def test_export_slash(state: ReplState, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
from plyngent.lmproto.openai_compatible.model import AssistantChatMessage, UserChatMessage
|
||||
|
||||
assert state.session_id is not None
|
||||
_ = await state.memory.append_message(state.session_id, UserChatMessage(content="hi"))
|
||||
_ = await state.memory.append_message(state.session_id, AssistantChatMessage(content="yo"))
|
||||
path = tmp_path / "out.md"
|
||||
assert await handle_slash(state, f"/export md {path}") is True
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "Session" in text
|
||||
assert "hi" in text
|
||||
assert "yo" in text
|
||||
assert str(path.resolve()) in capsys.readouterr().out
|
||||
|
||||
jpath = tmp_path / "out.json"
|
||||
assert await handle_slash(state, f"/export json {jpath}") is True
|
||||
raw = jpath.read_text(encoding="utf-8")
|
||||
assert '"session_id"' in raw
|
||||
assert "hi" in raw
|
||||
|
||||
|
||||
async def test_stream_toggle(state: ReplState) -> None:
|
||||
assert state.agent.stream is True
|
||||
assert await handle_slash(state, "/stream off") is True
|
||||
|
||||
@@ -98,6 +98,37 @@ async def test_session_workspace_binding(store: MemoryStore, tmp_path: object) -
|
||||
assert {s.sid for s in listed_b} == {sb.sid}
|
||||
|
||||
|
||||
async def test_rename_session(store: MemoryStore) -> None:
|
||||
session = await store.create_session(name="old")
|
||||
updated = await store.rename_session(session.sid, " new name ")
|
||||
assert updated.name == "new name"
|
||||
again = await store.get_session(session.sid)
|
||||
assert again is not None
|
||||
assert again.name == "new name"
|
||||
try:
|
||||
await store.rename_session(session.sid, " ")
|
||||
raise AssertionError("expected ValueError")
|
||||
except ValueError as exc:
|
||||
assert "non-empty" in str(exc)
|
||||
try:
|
||||
await store.rename_session(999_999, "x")
|
||||
raise AssertionError("expected ValueError")
|
||||
except ValueError as exc:
|
||||
assert "not found" in str(exc)
|
||||
|
||||
|
||||
async def test_delete_session_cascades_messages(store: MemoryStore) -> None:
|
||||
from plyngent.lmproto.openai_compatible.model import UserChatMessage
|
||||
|
||||
session = await store.create_session(name="gone")
|
||||
_ = await store.append_message(session.sid, UserChatMessage(content="a"))
|
||||
_ = await store.append_message(session.sid, UserChatMessage(content="b"))
|
||||
assert await store.delete_session(session.sid) is True
|
||||
assert await store.get_session(session.sid) is None
|
||||
assert await store.list_messages(session.sid) == []
|
||||
assert await store.delete_session(session.sid) is False
|
||||
|
||||
|
||||
async def test_update_session_workspace(store: MemoryStore, tmp_path: object) -> None:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
Reference in New Issue
Block a user