docs(changelog): stamp #6892 + add direct sync_session_title coverage tests

Codex round-1 SHOULD-FIX: the PR's only test change mocked the new write to a
no-op. Add real-SessionDB tests proving generated-title persistence, manual-rename
no-overwrite, and cli/gateway source preservation on ensure_session. Additive
test-only; no behavior change to the contributor's fix.

Co-authored-by: liuguangyong93 <liuguangyong93@users.noreply.github.com>
This commit is contained in:
nesquena-hermesandliuguangyong93 committed 2026-08-12 18:51:46 +00:00
1 parent 32b7f0a5e0
commit d73cea803e
2 files changed
+96

No files matched your search

+1
View File
@@ -5,6 +5,7 @@
### Fixed ### 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) - **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) - **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)
@@ -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()