mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
e61a405add
Batch release v0.50.231 — 3 fixes. ## PRs included | PR | Author | Fix | |---|---|---| | #1186 | @nesquena (Claude Code) | macOS `/etc` symlink bypass in workspace blocked-roots | | #1187 | @nesquena-hermes | Workspace panel stuck closed after empty-session reload | | #1190 | @bergeouss | Fenced code content leaking into markdown passes (#1154) | All three PRs were independently reviewed and approved by @nesquena. ## Test results **2729 passed, 2 skipped** (2 macOS-only tests correctly skipped on Linux). Browser QA: **21/21**. ## Key fix notes **#1186:** `_workspace_blocked_roots()` now returns both literal and `Path.resolve()` forms of each blocked root. macOS symlinks (`/etc → /private/etc`) previously let a resolved candidate slip past the literal check. New `_is_blocked_system_path()` helper with `/var/folders` and `/var/tmp` carve-outs for pytest temp dirs. **#1187:** Regression from #1182 — `syncWorkspacePanelState()` force-closed on any no-session state. Now only closes in `'preview'` mode. Both boot paths restore localStorage panel pref before sync. **#1190:** Fenced code blocks are now stashed as `\x00P<n>\x00` tokens through ALL markdown passes (list/heading/table regexes), restored at the very end. Previously, diff hunks and markdown headings inside code blocks triggered those regexes, injecting `<ul>/<li>/<h>` tags that broke `</pre>` closure.
139 lines
6.7 KiB
Python
139 lines
6.7 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")
|
|
|
|
|
|
# ── 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"
|
|
)
|