feat: add history UX and expand retention-focused roadmap
Some checks failed
CI / test (push) Failing after 15s
Some checks failed
CI / test (push) Failing after 15s
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
@@ -338,3 +339,108 @@ def test_interactive_rag_debug_prints_retrieval_scores(monkeypatch) -> None: #
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "RAG retrieve:" in result.stdout
|
||||
|
||||
|
||||
def test_history_command_lists_sessions(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
class FakeStore:
|
||||
def __init__(self, _path: str) -> None:
|
||||
pass
|
||||
|
||||
def list_recent(self, *, host: str | None = None, limit: int = 20):
|
||||
del limit
|
||||
if host == "web01":
|
||||
return [
|
||||
SimpleNamespace(
|
||||
session_id="20260507T120000Z",
|
||||
host="web01",
|
||||
issue="nginx down",
|
||||
summary="Root cause: bad config",
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("tai.cli.SessionStore", FakeStore)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["history", "--session-memory", "~/.tai/sessions", "--host", "web01"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "session(s)" in result.stdout
|
||||
assert "20260507T120000Z" in result.stdout
|
||||
|
||||
|
||||
def test_history_command_exports_markdown(monkeypatch, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
||||
class FakeStore:
|
||||
def __init__(self, _path: str) -> None:
|
||||
pass
|
||||
|
||||
def list_recent(self, *, host: str | None = None, limit: int = 20):
|
||||
del host, limit
|
||||
return [
|
||||
SimpleNamespace(
|
||||
session_id="20260507T120000Z",
|
||||
host="web01",
|
||||
issue="nginx down",
|
||||
summary="Root cause: bad config",
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr("tai.cli.SessionStore", FakeStore)
|
||||
export_path = tmp_path / "history.md"
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["history", "--session-memory", "~/.tai/sessions", "--export", str(export_path)],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Exported" in result.stdout
|
||||
text = export_path.read_text(encoding="utf-8")
|
||||
assert "# tai session history" in text
|
||||
assert "nginx down" in text
|
||||
|
||||
|
||||
def test_interactive_history_without_store_shows_hint(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||
_mock_session(monkeypatch)
|
||||
|
||||
async def fake_collect_from_plan(_session, _plan) -> CollectionReport: # type: ignore[no-untyped-def]
|
||||
return CollectionReport(
|
||||
host="ssh.archflux.net",
|
||||
items=[
|
||||
CollectedItem(
|
||||
name="kernel",
|
||||
result=SSHCommandResult(
|
||||
command="uname -a",
|
||||
exit_code=0,
|
||||
stdout="Linux test",
|
||||
stderr="",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
commands = iter(["/history", "/quit"])
|
||||
monkeypatch.setattr("tai.cli.collect_from_plan", fake_collect_from_plan)
|
||||
monkeypatch.setattr("tai.cli.console.input", lambda _prompt: next(commands))
|
||||
monkeypatch.setattr("tai.cli._stdin_is_tty", lambda: True)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"run", "apache failed",
|
||||
"--host",
|
||||
"ssh.archflux.net",
|
||||
"--port",
|
||||
"5566",
|
||||
"--no-probe",
|
||||
"--interactive",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Session memory is disabled" in result.stdout
|
||||
|
||||
@@ -77,3 +77,53 @@ def test_query_returns_past_sessions(tmp_path: Path) -> None:
|
||||
assert isinstance(results[0], PastSession)
|
||||
assert results[0].host == "web01"
|
||||
assert "package missing" in results[0].summary
|
||||
|
||||
|
||||
def test_list_recent_returns_sessions_sorted_desc(tmp_path: Path) -> None:
|
||||
chroma_mock = _make_chromadb_mock()
|
||||
collection = chroma_mock.PersistentClient.return_value.get_or_create_collection.return_value
|
||||
collection.count.return_value = 3
|
||||
collection.get.return_value = {
|
||||
"ids": ["20260506T120000Z", "20260507T120000Z", "20260505T120000Z"],
|
||||
"documents": ["older", "newer", "oldest"],
|
||||
"metadatas": [
|
||||
{"host": "web01", "issue": "i1"},
|
||||
{"host": "web01", "issue": "i2"},
|
||||
{"host": "db01", "issue": "i3"},
|
||||
],
|
||||
}
|
||||
|
||||
with patch.dict("sys.modules", {"chromadb": chroma_mock}):
|
||||
store = SessionStore(tmp_path / "store")
|
||||
results = store.list_recent(limit=2)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0].session_id == "20260507T120000Z"
|
||||
assert results[1].session_id == "20260506T120000Z"
|
||||
|
||||
|
||||
def test_search_keyword_filters_by_term_and_host(tmp_path: Path) -> None:
|
||||
chroma_mock = _make_chromadb_mock()
|
||||
collection = chroma_mock.PersistentClient.return_value.get_or_create_collection.return_value
|
||||
collection.count.return_value = 3
|
||||
collection.get.return_value = {
|
||||
"ids": ["20260505T120000Z", "20260506T120000Z", "20260507T120000Z"],
|
||||
"documents": [
|
||||
"Root cause: nginx config typo",
|
||||
"Root cause: package missing",
|
||||
"Root cause: nginx port conflict",
|
||||
],
|
||||
"metadatas": [
|
||||
{"host": "web01", "issue": "nginx fails"},
|
||||
{"host": "web01", "issue": "sssd fails"},
|
||||
{"host": "db01", "issue": "nginx start failed"},
|
||||
],
|
||||
}
|
||||
|
||||
with patch.dict("sys.modules", {"chromadb": chroma_mock}):
|
||||
store = SessionStore(tmp_path / "store")
|
||||
results = store.search_keyword("nginx", host="web01", limit=5)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].host == "web01"
|
||||
assert "nginx" in results[0].issue.lower()
|
||||
|
||||
Reference in New Issue
Block a user