mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-11 18:20:30 +00:00
4cf40a317a
## Release v0.51.264 — Release IF (stage-r14) Un-held sibling pair (#3585 + #3586) — both addressed the findings from the earlier hold; re-reviewed fresh. ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3585 | @rodboev | Cron sessions no longer flood the CLI sidebar window (restored the `("cron","webui")` exclusion in `_load_cli_sessions_uncached`). | | #3586 | @rodboev | Messaging sessions keep their source label after a refresh **and open + send correctly** — `is_cli_session_row()` classifies them non-CLI, and the sidebar open/import path now uses `_isMessagingSession()` so a reclassified Discord/Telegram/Slack row is imported on open (no transient stub → no `/api/chat/start` 404). | ### Un-hold note These were held earlier today because the `is_cli_session_row()` reclassification (#3586) created a CORE open-path regression — opening a reclassified messaging session 404'd on the next send. The author pushed a fix adding the `_isMessagingSession()` import gate at all open/lineage/refresh paths (+ regression test `test_issue3603_external_session_import_gate.py`), and Codex confirmed both that AND the secondary webui-recovery concern (cron-only exclusion now keeps `source='webui'` sidecar-less recovery rows) are resolved. ### Gate - Full pytest suite: **7729 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** — open→import→send path verified (messaging rows go through `/api/session/import_cli` before `/api/chat/start`); `is_cli_session_row` classification correct; the pair composes in `_load_cli_sessions_uncached`. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
"""Regression coverage for is_cli_session wrongly True for messaging sessions (#3586).
|
|
|
|
_load_cli_sessions_uncached() previously hardcoded 'is_cli_session': True for
|
|
every state.db row. Sessions from Discord, Telegram, Slack, and cron must have
|
|
is_cli_session == False; only rows whose source normalises to 'cli' should be True.
|
|
"""
|
|
|
|
import sqlite3
|
|
import time
|
|
import unittest.mock
|
|
|
|
import api.models as models
|
|
|
|
|
|
def _make_state_db(path, sessions):
|
|
"""Create a minimal state.db with the given sessions list.
|
|
|
|
Each item is a dict with at least: id, source, title.
|
|
Messaging sessions get one user + one assistant message (enough to be
|
|
importable since they are not CLI and bypass the user-turn threshold).
|
|
CLI sessions get a non-generic title so is_cli_session_row_visible does
|
|
not apply the CLI_MIN_UNTITLED_USER_MESSAGE_COUNT guard; they also get
|
|
two user messages as belt-and-suspenders.
|
|
"""
|
|
conn = sqlite3.connect(str(path))
|
|
conn.executescript(
|
|
"""
|
|
CREATE TABLE sessions (
|
|
id TEXT PRIMARY KEY,
|
|
source TEXT,
|
|
session_source TEXT,
|
|
title TEXT,
|
|
model TEXT,
|
|
started_at REAL NOT NULL,
|
|
message_count INTEGER DEFAULT 0,
|
|
parent_session_id TEXT,
|
|
ended_at REAL,
|
|
end_reason TEXT
|
|
);
|
|
CREATE INDEX idx_sessions_started ON sessions(started_at);
|
|
CREATE TABLE messages (
|
|
id TEXT PRIMARY KEY,
|
|
session_id TEXT,
|
|
role TEXT,
|
|
content TEXT,
|
|
timestamp REAL
|
|
);
|
|
CREATE INDEX idx_messages_session ON messages(session_id, timestamp);
|
|
"""
|
|
)
|
|
base = time.time() - len(sessions)
|
|
for i, sess in enumerate(sessions):
|
|
sid = sess['id']
|
|
started = base + i
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO sessions
|
|
(id, source, session_source, title, model, started_at, message_count,
|
|
parent_session_id, ended_at, end_reason)
|
|
VALUES (?, ?, NULL, ?, 'deepseek/deepseek-chat', ?, 2, NULL, NULL, NULL)
|
|
""",
|
|
(sid, sess['source'], sess.get('title', sid), started),
|
|
)
|
|
# Two user messages so CLI rows pass CLI_MIN_UNTITLED_USER_MESSAGE_COUNT (=2).
|
|
conn.execute(
|
|
"INSERT INTO messages (id, session_id, role, content, timestamp) VALUES (?, ?, 'user', 'hello', ?)",
|
|
(f"msg_{sid}_0", sid, started),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO messages (id, session_id, role, content, timestamp) VALUES (?, ?, 'user', 'follow-up', ?)",
|
|
(f"msg_{sid}_1", sid, started + 0.1),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO messages (id, session_id, role, content, timestamp) VALUES (?, ?, 'assistant', 'hi', ?)",
|
|
(f"msg_{sid}_2", sid, started + 0.2),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
def _call_uncached(tmp_path, sessions):
|
|
"""Build a state.db from ``sessions``, call _load_cli_sessions_uncached, return results."""
|
|
db = tmp_path / 'state.db'
|
|
_make_state_db(db, sessions)
|
|
hermes_home = tmp_path
|
|
|
|
# Patch out side-effects that touch the real filesystem.
|
|
with (
|
|
unittest.mock.patch('api.models.get_claude_code_sessions', return_value=[]),
|
|
unittest.mock.patch('api.models.ensure_cron_project', return_value='cron-project-id'),
|
|
):
|
|
return models._load_cli_sessions_uncached(hermes_home, db, None)
|
|
|
|
|
|
def _find(results, sid):
|
|
return next((s for s in results if s['session_id'] == sid), None)
|
|
|
|
|
|
def test_discord_session_is_not_cli(tmp_path):
|
|
"""Discord-sourced sessions must have is_cli_session == False."""
|
|
sessions = [
|
|
{'id': 'discord_abc123', 'source': 'discord', 'title': 'Discord Chat'},
|
|
]
|
|
results = _call_uncached(tmp_path, sessions)
|
|
row = _find(results, 'discord_abc123')
|
|
assert row is not None, "discord session should appear in results"
|
|
assert row['is_cli_session'] is False, (
|
|
f"expected is_cli_session=False for discord session, got {row['is_cli_session']!r}"
|
|
)
|
|
|
|
|
|
def test_telegram_session_is_not_cli(tmp_path):
|
|
"""Telegram-sourced sessions must have is_cli_session == False."""
|
|
sessions = [
|
|
{'id': 'telegram_xyz789', 'source': 'telegram', 'title': 'Telegram Chat'},
|
|
]
|
|
results = _call_uncached(tmp_path, sessions)
|
|
row = _find(results, 'telegram_xyz789')
|
|
assert row is not None, "telegram session should appear in results"
|
|
assert row['is_cli_session'] is False, (
|
|
f"expected is_cli_session=False for telegram session, got {row['is_cli_session']!r}"
|
|
)
|
|
|
|
|
|
def test_slack_session_is_not_cli(tmp_path):
|
|
"""Slack-sourced sessions must have is_cli_session == False."""
|
|
sessions = [
|
|
{'id': 'slack_def456', 'source': 'slack', 'title': 'Slack Chat'},
|
|
]
|
|
results = _call_uncached(tmp_path, sessions)
|
|
row = _find(results, 'slack_def456')
|
|
assert row is not None, "slack session should appear in results"
|
|
assert row['is_cli_session'] is False, (
|
|
f"expected is_cli_session=False for slack session, got {row['is_cli_session']!r}"
|
|
)
|
|
|
|
|
|
def test_cron_session_is_not_cli(tmp_path):
|
|
"""Cron-sourced sessions must have is_cli_session == False."""
|
|
sessions = [
|
|
{'id': 'cron_job1_1717000000', 'source': 'cron', 'title': 'Scheduled Job'},
|
|
]
|
|
results = _call_uncached(tmp_path, sessions)
|
|
row = _find(results, 'cron_job1_1717000000')
|
|
assert row is not None, "cron session should appear in results"
|
|
assert row['is_cli_session'] is False, (
|
|
f"expected is_cli_session=False for cron session, got {row['is_cli_session']!r}"
|
|
)
|
|
|
|
|
|
def test_cli_session_is_cli(tmp_path):
|
|
"""CLI-sourced sessions must have is_cli_session == True."""
|
|
sessions = [
|
|
# Non-generic title bypasses the _looks_like_default_cli_title guard;
|
|
# two user messages are inserted by _make_state_db for belt-and-suspenders.
|
|
{'id': 'cli_session_001', 'source': 'cli', 'title': 'Refactor auth module'},
|
|
]
|
|
results = _call_uncached(tmp_path, sessions)
|
|
row = _find(results, 'cli_session_001')
|
|
assert row is not None, "cli session should appear in results"
|
|
assert row['is_cli_session'] is True, (
|
|
f"expected is_cli_session=True for cli session, got {row['is_cli_session']!r}"
|
|
)
|
|
|
|
|
|
def test_mixed_sources_classified_correctly(tmp_path):
|
|
"""Mixed source types are each classified correctly in a single db scan."""
|
|
sessions = [
|
|
{'id': 'disc_1', 'source': 'discord', 'title': 'Discord'},
|
|
{'id': 'tele_1', 'source': 'telegram', 'title': 'Telegram'},
|
|
# Non-generic title so the CLI visibility check passes without extra tuning.
|
|
{'id': 'cli_1', 'source': 'cli', 'title': 'Improve parser performance'},
|
|
{'id': 'cron_job2_1717000001', 'source': 'cron', 'title': 'Cron'},
|
|
]
|
|
results = _call_uncached(tmp_path, sessions)
|
|
by_id = {s['session_id']: s for s in results}
|
|
|
|
assert by_id['disc_1']['is_cli_session'] is False
|
|
assert by_id['tele_1']['is_cli_session'] is False
|
|
assert by_id['cli_1']['is_cli_session'] is True
|
|
assert by_id['cron_job2_1717000001']['is_cli_session'] is False
|