diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d66a032..cedf4253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed +- **`hermes sessions list` no longer shows blank titles for WebUI sessions.** Background title generation wrote the auto-title to the WebUI session sidecar but never to hermes-agent's `state.db`, so the CLI/TUI session list showed those WebUI sessions untitled. The title is now bridged into `state.db` after it's generated (and on adaptive refresh) via `set_auto_title_if_empty`, which respects title provenance — a name you set manually via CLI/Gateway/TUI (`user` source) is never overwritten by the auto-title, an already-auto-titled row stays put, and an existing session's `source` is preserved (the sync creates the row with INSERT-OR-IGNORE semantics). Thanks @liuguangyong93. (#6892) - **No more phantom "Compressing context" barrier after switching sessions.** The compression UI state is per-session, but `loadSession()` did not clear it on a switch, so a compression card from a prior session could leak across the load and appear as a phantom "Compressing context" barrier on a fresh session that never triggered compression. Switching sessions now clears the compression UI (state, elapsed timer, composer session-lock, and barrier element) alongside the other per-session resets, before the transcript loads. A compression genuinely running on the session you switch *to* is unaffected — it re-surfaces from that session's own live event stream (and manual compression from its server job status). Thanks @happy5318. (#6938) - **Layout test helper no longer flags scroll-reachable fields as off-viewport.** The shared `assert_layout_sane` degenerate check reported a false "interactive element is off-viewport" for an input that sits below the fold at rest but is reachable by scrolling an `overflow-y:auto` ancestor (e.g. a tall dialog on a short landscape viewport). It now exempts elements whose off-viewport edge is absorbed by a scrollable ancestor, while still flagging genuinely-unreachable elements (no scroll escape). The Kanban board settings modal layout test also drops its synthetic 480×320 short-landscape case (a viewport no real device uses, whose sub-fold geometry was environment-marginal and CI-flaky); real desktop/tablet/narrow viewports still assert the layout. (internal test infra) diff --git a/tests/test_issue6892_sync_title_coverage.py b/tests/test_issue6892_sync_title_coverage.py new file mode 100644 index 00000000..c3319e17 --- /dev/null +++ b/tests/test_issue6892_sync_title_coverage.py @@ -0,0 +1,95 @@ +"""Direct coverage for api.state_sync.sync_session_title (#6892). + +Codex round-1 SHOULD-FIX: the PR's only test change mocks sync_session_title to a +no-op, so the new state.db-writing path had no direct test proving it actually +persists a generated title AND preserves a manual rename. These tests exercise +the real function against a temporary hermes-agent SessionDB. +""" + +import pytest + + +def _make_db(tmp_path): + hermes_state = pytest.importorskip("hermes_state") + SessionDB = hermes_state.SessionDB + return SessionDB(db_path=tmp_path / "state.db") + + +@pytest.mark.requires_agent_modules +def test_sync_session_title_persists_generated_title(tmp_path, monkeypatch): + """A generated title is written to state.db so `hermes sessions list` isn't blank.""" + from api import state_sync + + db = _make_db(tmp_path) + try: + # Route sync_session_title at the real temp DB (a fresh handle each call, + # mirroring production, but pointed at our tmp state.db). + monkeypatch.setattr( + state_sync, + "_get_state_db", + lambda profile=None: _make_db(tmp_path), + ) + + state_sync.sync_session_title("sess-gen", "Generated Title", profile="default") + + assert db.get_session_title("sess-gen") == "Generated Title" + # Provenance is the automatic LLM source, not a user rename. + assert db.get_session_title_source("sess-gen") == db.TITLE_SOURCE_LLM + finally: + db.close() + + +@pytest.mark.requires_agent_modules +def test_sync_session_title_does_not_overwrite_manual_rename(tmp_path, monkeypatch): + """A user rename (source=user) is never clobbered by a later title sync.""" + from api import state_sync + + db = _make_db(tmp_path) + try: + # Seed a manual rename first — this records `user` provenance. + db.ensure_session(session_id="sess-manual", source="cli") + db.set_session_title("sess-manual", "My Manual Name") + assert db.get_session_title_source("sess-manual") == db.TITLE_SOURCE_USER + + monkeypatch.setattr( + state_sync, + "_get_state_db", + lambda profile=None: _make_db(tmp_path), + ) + + # A background generation now tries to sync an auto title — must be a no-op. + state_sync.sync_session_title("sess-manual", "Auto Generated", profile="default") + + assert db.get_session_title("sess-manual") == "My Manual Name" + assert db.get_session_title_source("sess-manual") == db.TITLE_SOURCE_USER + finally: + db.close() + + +@pytest.mark.requires_agent_modules +def test_sync_session_title_preserves_existing_source_on_ensure(tmp_path, monkeypatch): + """ensure_session(source='webui') must not clobber an existing cli/gateway row's source.""" + from api import state_sync + + db = _make_db(tmp_path) + try: + db.ensure_session(session_id="sess-cli", source="cli") + + monkeypatch.setattr( + state_sync, + "_get_state_db", + lambda profile=None: _make_db(tmp_path), + ) + + # Sync (which calls ensure_session(source='webui')) on a pre-existing cli row. + state_sync.sync_session_title("sess-cli", "Some Title", profile="default") + + # INSERT OR IGNORE semantics: the original source is preserved, the title lands. + assert db.get_session_title("sess-cli") == "Some Title" + with db._lock: + row = db._conn.execute( + "SELECT source FROM sessions WHERE id = ?", ("sess-cli",) + ).fetchone() + assert row["source"] == "cli" + finally: + db.close()