""" 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("", 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