Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""Lifecycle status classification for session pickers.
|
||||
|
||||
Covers ``classify_session_status`` (pure last-message shape → status) and
|
||||
``SessionDB.session_lifecycle_statuses`` (batched last-message lookup), plus
|
||||
the delete wiring the picker's 'd' key relies on.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_state import (
|
||||
SESSION_STATUS_COMPLETE,
|
||||
SESSION_STATUS_EMPTY,
|
||||
SESSION_STATUS_ERROR,
|
||||
SESSION_STATUS_INTERRUPTED,
|
||||
SessionDB,
|
||||
classify_session_status,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path):
|
||||
database = SessionDB(tmp_path / "state.db")
|
||||
try:
|
||||
yield database
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure classifier
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role,has_tool_calls,finish_reason,expected",
|
||||
[
|
||||
("assistant", False, "stop", SESSION_STATUS_COMPLETE),
|
||||
("assistant", False, None, SESSION_STATUS_COMPLETE),
|
||||
("assistant", False, "length", SESSION_STATUS_COMPLETE),
|
||||
("assistant", True, "tool_calls", SESSION_STATUS_INTERRUPTED),
|
||||
("user", False, None, SESSION_STATUS_INTERRUPTED),
|
||||
("tool", False, None, SESSION_STATUS_INTERRUPTED),
|
||||
("assistant", False, "error", SESSION_STATUS_ERROR),
|
||||
("assistant", True, "error", SESSION_STATUS_ERROR),
|
||||
("user", False, "agent_error", SESSION_STATUS_ERROR),
|
||||
("system", False, None, SESSION_STATUS_COMPLETE),
|
||||
(None, False, None, SESSION_STATUS_COMPLETE),
|
||||
],
|
||||
)
|
||||
def test_classify_session_status(role, has_tool_calls, finish_reason, expected):
|
||||
assert classify_session_status(role, has_tool_calls, finish_reason) == expected
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DB-backed batch classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_session_lifecycle_statuses_shapes(db):
|
||||
# complete: normal user → assistant exchange
|
||||
db.create_session("s_complete", source="cli")
|
||||
db.append_message("s_complete", "user", "hi")
|
||||
db.append_message("s_complete", "assistant", "hello", finish_reason="stop")
|
||||
|
||||
# interrupted: user asked, no reply landed
|
||||
db.create_session("s_user_tail", source="cli")
|
||||
db.append_message("s_user_tail", "user", "are you there?")
|
||||
|
||||
# interrupted: assistant fired tool calls, no tool result followed
|
||||
db.create_session("s_pending_tool", source="cli")
|
||||
db.append_message("s_pending_tool", "user", "run it")
|
||||
db.append_message(
|
||||
"s_pending_tool",
|
||||
"assistant",
|
||||
None,
|
||||
tool_calls=[{"id": "c1", "function": {"name": "terminal", "arguments": "{}"}}],
|
||||
finish_reason="tool_calls",
|
||||
)
|
||||
|
||||
# complete: full tool round-trip then final assistant reply
|
||||
db.create_session("s_tool_roundtrip", source="cli")
|
||||
db.append_message("s_tool_roundtrip", "user", "run it")
|
||||
db.append_message(
|
||||
"s_tool_roundtrip",
|
||||
"assistant",
|
||||
None,
|
||||
tool_calls=[{"id": "c1", "function": {"name": "terminal", "arguments": "{}"}}],
|
||||
finish_reason="tool_calls",
|
||||
)
|
||||
db.append_message("s_tool_roundtrip", "tool", "ok", tool_call_id="c1")
|
||||
db.append_message("s_tool_roundtrip", "assistant", "done", finish_reason="stop")
|
||||
|
||||
# interrupted: tool result present but assistant never consumed it
|
||||
db.create_session("s_tool_tail", source="cli")
|
||||
db.append_message("s_tool_tail", "user", "run it")
|
||||
db.append_message(
|
||||
"s_tool_tail",
|
||||
"assistant",
|
||||
None,
|
||||
tool_calls=[{"id": "c2", "function": {"name": "terminal", "arguments": "{}"}}],
|
||||
finish_reason="tool_calls",
|
||||
)
|
||||
db.append_message("s_tool_tail", "tool", "ok", tool_call_id="c2")
|
||||
|
||||
# error: last message carries an error finish_reason
|
||||
db.create_session("s_error", source="cli")
|
||||
db.append_message("s_error", "user", "hi")
|
||||
db.append_message("s_error", "assistant", "boom", finish_reason="error")
|
||||
|
||||
# empty: session row exists, zero messages
|
||||
db.create_session("s_empty", source="cli")
|
||||
|
||||
statuses = db.session_lifecycle_statuses(
|
||||
[
|
||||
"s_complete",
|
||||
"s_user_tail",
|
||||
"s_pending_tool",
|
||||
"s_tool_roundtrip",
|
||||
"s_tool_tail",
|
||||
"s_error",
|
||||
"s_empty",
|
||||
]
|
||||
)
|
||||
assert statuses == {
|
||||
"s_complete": SESSION_STATUS_COMPLETE,
|
||||
"s_user_tail": SESSION_STATUS_INTERRUPTED,
|
||||
"s_pending_tool": SESSION_STATUS_INTERRUPTED,
|
||||
"s_tool_roundtrip": SESSION_STATUS_COMPLETE,
|
||||
"s_tool_tail": SESSION_STATUS_INTERRUPTED,
|
||||
"s_error": SESSION_STATUS_ERROR,
|
||||
"s_empty": SESSION_STATUS_EMPTY,
|
||||
}
|
||||
|
||||
|
||||
def test_session_lifecycle_statuses_empty_input(db):
|
||||
assert db.session_lifecycle_statuses([]) == {}
|
||||
assert db.session_lifecycle_statuses([None, ""]) == {}
|
||||
|
||||
|
||||
def test_session_lifecycle_statuses_unknown_id(db):
|
||||
# Unknown ids classify as 'empty' (no messages), never raise.
|
||||
assert db.session_lifecycle_statuses(["nope"]) == {
|
||||
"nope": SESSION_STATUS_EMPTY
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Picker helpers (status annotation + delete wiring)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_annotate_session_statuses(db):
|
||||
from hermes_cli.main import _annotate_session_statuses, _session_status_tag
|
||||
|
||||
db.create_session("s1", source="cli")
|
||||
db.append_message("s1", "user", "hi")
|
||||
db.append_message("s1", "assistant", "hello", finish_reason="stop")
|
||||
db.create_session("s2", source="cli")
|
||||
db.append_message("s2", "user", "hi")
|
||||
|
||||
rows = [{"id": "s1"}, {"id": "s2"}]
|
||||
_annotate_session_statuses(rows, db)
|
||||
assert rows[0]["_status"] == SESSION_STATUS_COMPLETE
|
||||
assert rows[1]["_status"] == SESSION_STATUS_INTERRUPTED
|
||||
|
||||
# No db → rows untouched, tag falls back to '-'
|
||||
bare = [{"id": "s1"}]
|
||||
_annotate_session_statuses(bare, None)
|
||||
assert "_status" not in bare[0]
|
||||
assert _session_status_tag(bare[0].get("_status")) == "-"
|
||||
|
||||
# Tag mapping
|
||||
assert _session_status_tag(SESSION_STATUS_COMPLETE) == "done"
|
||||
assert _session_status_tag(SESSION_STATUS_INTERRUPTED) == "intr"
|
||||
assert _session_status_tag(SESSION_STATUS_ERROR) == "err"
|
||||
assert _session_status_tag(SESSION_STATUS_EMPTY) == "empty"
|
||||
|
||||
|
||||
def test_delete_session_removes_session_and_messages(db, tmp_path):
|
||||
db.create_session("doomed", source="cli")
|
||||
db.append_message("doomed", "user", "hi")
|
||||
db.append_message("doomed", "assistant", "hello", finish_reason="stop")
|
||||
|
||||
assert db.delete_session("doomed", sessions_dir=tmp_path / "sessions") is True
|
||||
assert db.get_session("doomed") is None
|
||||
remaining = db._conn.execute(
|
||||
"SELECT COUNT(*) FROM messages WHERE session_id = ?", ("doomed",)
|
||||
).fetchone()[0]
|
||||
assert remaining == 0
|
||||
# Deleting again reports False (not found)
|
||||
assert db.delete_session("doomed") is False
|
||||
Reference in New Issue
Block a user