* Refresh Russian localization bundle * feat(#5682): honor profile query during boot * fix(profile): refresh deep-link reasoning state (#5682) * test(ci): initialize reasoning transition harness state * test(ci): accept reasoning fetch context override * fix(#5682): preserve retryable profile query intent * test(#5682): accept profile switch result contract * fix(#5682): bind reasoning fetches to profile transitions * Release exp-v0.52.42: ?profile= deep links (#5730) + Russian locale refresh (#5778) --------- Co-authored-by: DrMaks22 <DrMaks22@users.noreply.github.com> Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
183 lines
9.1 KiB
Python
183 lines
9.1 KiB
Python
"""
|
|
Regression tests for the workspace panel persisting across page reload on
|
|
empty-session and no-session boot paths.
|
|
|
|
Two boot paths previously dropped the workspace panel even when the user had
|
|
explicitly opened it before reloading:
|
|
|
|
1. Ephemeral-session guard added in #1182: when the restored session has
|
|
0 messages, boot clears localStorage and shows the empty state. This
|
|
path was calling ``syncWorkspacePanelState()`` without first restoring
|
|
``_workspacePanelMode`` from localStorage.
|
|
2. No-saved-session path: a fresh page load with no localStorage session
|
|
also went straight to ``syncWorkspacePanelState()`` without restoring
|
|
the panel preference.
|
|
|
|
Both paths force-closed the panel because ``syncWorkspacePanelState()``
|
|
unconditionally set ``_workspacePanelMode='closed'`` whenever ``S.session``
|
|
was null — even when the user's preference was 'open'.
|
|
|
|
Fix verified by these tests:
|
|
|
|
- ``syncWorkspacePanelState`` checks ``_workspacePanelMode==='preview'``
|
|
BEFORE force-closing, so 'browse' mode is preserved without a session.
|
|
- Both boot paths read the panel pref from localStorage and set
|
|
``_workspacePanelMode='browse'`` before calling sync.
|
|
- ``canBrowse`` and ``openWorkspacePanel()`` include
|
|
``S._profileDefaultWorkspace`` so the toggle stays enabled.
|
|
"""
|
|
import pathlib
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
|
|
I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _html_tag_by_id(element_id: str) -> str:
|
|
marker = f'id="{element_id}"'
|
|
idx = INDEX_HTML.find(marker)
|
|
assert idx > 0, f"{element_id} not found in index.html"
|
|
start = INDEX_HTML.rfind("<button", 0, idx)
|
|
end = INDEX_HTML.find(">", idx)
|
|
assert start >= 0 and end > idx, f"{element_id} button tag not found"
|
|
return INDEX_HTML[start : end + 1]
|
|
|
|
|
|
# ── 1. syncWorkspacePanelState preserves browse mode without a session ──────
|
|
|
|
|
|
class TestSyncStateNoSession:
|
|
def test_preview_mode_without_session_force_closes(self):
|
|
"""A 'preview' panel needs file content from a session — close it
|
|
when there's no session."""
|
|
idx = BOOT_JS.find("function syncWorkspacePanelState()")
|
|
body = BOOT_JS[idx:idx + 800]
|
|
assert "_workspacePanelMode==='preview'" in body, (
|
|
"syncWorkspacePanelState must check _workspacePanelMode==='preview' "
|
|
"before force-closing on no-session boot"
|
|
)
|
|
assert "_setWorkspacePanelMode('closed')" in body, (
|
|
"syncWorkspacePanelState still must close 'preview' mode without a session"
|
|
)
|
|
|
|
def test_browse_mode_calls_sync_ui_instead_of_force_close(self):
|
|
"""For 'browse' mode without a session, syncWorkspacePanelUI() should
|
|
run so the panel renders its 'no workspace' or default-workspace state
|
|
rather than being force-closed."""
|
|
idx = BOOT_JS.find("function syncWorkspacePanelState()")
|
|
body = BOOT_JS[idx:idx + 800]
|
|
# The else branch (browse / closed mode without session) calls UI sync
|
|
assert "syncWorkspacePanelUI()" in body, (
|
|
"syncWorkspacePanelState must call syncWorkspacePanelUI() in the "
|
|
"no-session, non-preview branch so 'browse' mode is preserved"
|
|
)
|
|
|
|
|
|
# ── 2. Both boot paths restore panelPref before sync ────────────────────────
|
|
|
|
|
|
class TestBootPathsRestorePanelPref:
|
|
PREF_PATTERN = "hermes-webui-workspace-panel-pref"
|
|
|
|
def test_ephemeral_path_restores_panel_pref(self):
|
|
"""The empty-session guard (#1182) must read panelPref before
|
|
calling syncWorkspacePanelState()."""
|
|
# Find the ephemeral guard — it's marked by message_count===0 check
|
|
eph_idx = BOOT_JS.find("(S.session.message_count||0) === 0")
|
|
assert eph_idx > 0, "Empty-session guard not found in boot IIFE"
|
|
# The next syncWorkspacePanelState() call after this point is in the ephemeral path
|
|
sync_idx = BOOT_JS.find("syncWorkspacePanelState()", eph_idx)
|
|
assert sync_idx > 0, "syncWorkspacePanelState call not found in ephemeral path"
|
|
# panelPref must be read between the guard and the sync call
|
|
block = BOOT_JS[eph_idx:sync_idx]
|
|
assert self.PREF_PATTERN in block, (
|
|
"Ephemeral-session boot path must read 'hermes-webui-workspace-panel-pref' "
|
|
"from localStorage before calling syncWorkspacePanelState()"
|
|
)
|
|
assert "_workspacePanelMode='browse'" in block or "_workspacePanelMode = 'browse'" in block, (
|
|
"Ephemeral-session path must set _workspacePanelMode='browse' "
|
|
"when the pref is 'open'"
|
|
)
|
|
|
|
def test_no_session_path_restores_panel_pref(self):
|
|
"""The fresh-load (no localStorage session) path must read panelPref
|
|
before calling syncWorkspacePanelState()."""
|
|
# Find the comment marker that precedes the no-session path
|
|
marker = "no saved session"
|
|
m_idx = BOOT_JS.find(marker)
|
|
assert m_idx > 0, "no-saved-session path not found"
|
|
# syncWorkspacePanelState should appear shortly after
|
|
sync_idx = BOOT_JS.find("syncWorkspacePanelState()", m_idx)
|
|
assert sync_idx > 0, "syncWorkspacePanelState() not found after no-saved-session marker"
|
|
block = BOOT_JS[m_idx:sync_idx]
|
|
assert self.PREF_PATTERN in block, (
|
|
"No-saved-session boot path must read 'hermes-webui-workspace-panel-pref' "
|
|
"before calling syncWorkspacePanelState()"
|
|
)
|
|
assert "_workspacePanelMode='browse'" in block or "_workspacePanelMode = 'browse'" in block, (
|
|
"No-saved-session path must set _workspacePanelMode='browse' "
|
|
"when the pref is 'open'"
|
|
)
|
|
|
|
|
|
# ── 3. Toggle button stays enabled when profile default workspace exists ────
|
|
|
|
|
|
class TestToggleStaysEnabledWithProfileWorkspace:
|
|
def test_can_browse_includes_profile_default_workspace(self):
|
|
"""The toggle button's enabled state (canBrowse) must be true when
|
|
S._profileDefaultWorkspace is set, even with no active session."""
|
|
idx = BOOT_JS.find("const canBrowse=")
|
|
assert idx > 0, "canBrowse declaration not found in syncWorkspacePanelUI"
|
|
line = BOOT_JS[idx:idx + 200].split("\n", 1)[0]
|
|
assert "_profileDefaultWorkspace" in line, (
|
|
"canBrowse must include S._profileDefaultWorkspace so the toggle "
|
|
"button stays enabled when a profile workspace is configured"
|
|
)
|
|
|
|
def test_open_workspace_panel_allows_browse_with_profile_workspace(self):
|
|
"""openWorkspacePanel('browse') must not return early when
|
|
S._profileDefaultWorkspace is set, otherwise clicking the toggle
|
|
won't open the panel even though canBrowse said it should."""
|
|
idx = BOOT_JS.find("function openWorkspacePanel(")
|
|
body = BOOT_JS[idx:idx + 600]
|
|
# The early-return guard should include the profile-workspace check
|
|
assert "_profileDefaultWorkspace" in body, (
|
|
"openWorkspacePanel must include S._profileDefaultWorkspace in its "
|
|
"early-return guard so users can open the panel via the toggle "
|
|
"button when a profile workspace is configured"
|
|
)
|
|
|
|
|
|
class TestWorkspacePanelStatefulLocalization:
|
|
def test_locale_pass_resyncs_stateful_panel_tooltips(self):
|
|
"""Locale re-stamping must finish by restoring open/closed panel labels."""
|
|
assert "if (typeof syncWorkspacePanelUI === 'function') syncWorkspacePanelUI();" in I18N_JS
|
|
|
|
def test_stateful_toggle_buttons_do_not_carry_static_title_keys(self):
|
|
"""The show/hide tooltip belongs to syncWorkspacePanelUI(), not static DOM i18n."""
|
|
for element_id in ("btnWorkspacePanelToggle", "btnWorkspacePanelEdgeToggle"):
|
|
tag = _html_tag_by_id(element_id)
|
|
assert 'data-i18n-title="workspace_panel_show"' not in tag
|
|
assert 'data-i18n-title="workspace_panel_hide"' not in tag
|
|
|
|
def test_stateful_toggle_buttons_do_not_carry_static_aria_keys(self):
|
|
"""The final accessible label should match the current show/hide action."""
|
|
for element_id in ("btnWorkspacePanelToggle", "btnWorkspacePanelEdgeToggle"):
|
|
tag = _html_tag_by_id(element_id)
|
|
assert 'data-i18n-aria-label="workspace_panel_toggle"' not in tag
|
|
assert 'data-i18n-aria-label="workspace_panel_show"' not in tag
|
|
assert 'data-i18n-aria-label="workspace_panel_hide"' not in tag
|
|
assert "toggleBtn.setAttribute('aria-label', label);" in BOOT_JS
|
|
assert "edgeToggleBtn.setAttribute('aria-label', label);" in BOOT_JS
|
|
|
|
def test_clear_preview_accessible_label_matches_stateful_tooltip(self):
|
|
"""The close/preview action should expose the same localized label everywhere."""
|
|
idx = BOOT_JS.find("const clearBtn=$('btnClearPreview');")
|
|
assert idx > 0, "btnClearPreview sync block not found"
|
|
body = BOOT_JS[idx:idx + 600]
|
|
assert "const label=hasPreview?" in body
|
|
assert "_setButtonTooltip(clearBtn, label);" in body
|
|
assert "clearBtn.setAttribute('aria-label', label);" in body
|