Skip to content

Commit 2fbbb8d

Browse files
nesquenaclaude
andcommitted
test: lock workspace-panel persists-across-reload contract (#1187)
Static regression tests asserting the four invariants that prevent the workspace panel from being silently force-closed on empty-session and no-session boot paths: 1. syncWorkspacePanelState force-closes only 'preview' mode without a session — 'browse' mode runs through syncWorkspacePanelUI() so the panel renders rather than vanishes. 2. Both the empty-session guard path (#1182) and the no-saved-session path read 'hermes-webui-workspace-panel-pref' from localStorage before calling syncWorkspacePanelState(). 3. canBrowse in syncWorkspacePanelUI() includes S._profileDefaultWorkspace so the toggle button stays enabled when a profile workspace is configured. 4. openWorkspacePanel('browse') early-return guard also includes S._profileDefaultWorkspace so the toggle button can actually open the panel. These tests would have caught the original bug introduced when the empty-session guard was added in #1182 without the corresponding panel pref restoration. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 0f1f567 commit 2fbbb8d

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""
2+
Regression tests for the workspace panel persisting across page reload on
3+
empty-session and no-session boot paths.
4+
5+
Two boot paths previously dropped the workspace panel even when the user had
6+
explicitly opened it before reloading:
7+
8+
1. Ephemeral-session guard added in #1182: when the restored session has
9+
0 messages, boot clears localStorage and shows the empty state. This
10+
path was calling ``syncWorkspacePanelState()`` without first restoring
11+
``_workspacePanelMode`` from localStorage.
12+
2. No-saved-session path: a fresh page load with no localStorage session
13+
also went straight to ``syncWorkspacePanelState()`` without restoring
14+
the panel preference.
15+
16+
Both paths force-closed the panel because ``syncWorkspacePanelState()``
17+
unconditionally set ``_workspacePanelMode='closed'`` whenever ``S.session``
18+
was null — even when the user's preference was 'open'.
19+
20+
Fix verified by these tests:
21+
22+
- ``syncWorkspacePanelState`` checks ``_workspacePanelMode==='preview'``
23+
BEFORE force-closing, so 'browse' mode is preserved without a session.
24+
- Both boot paths read the panel pref from localStorage and set
25+
``_workspacePanelMode='browse'`` before calling sync.
26+
- ``canBrowse`` and ``openWorkspacePanel()`` include
27+
``S._profileDefaultWorkspace`` so the toggle stays enabled.
28+
"""
29+
import pathlib
30+
31+
REPO = pathlib.Path(__file__).parent.parent
32+
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
33+
34+
35+
# ── 1. syncWorkspacePanelState preserves browse mode without a session ──────
36+
37+
38+
class TestSyncStateNoSession:
39+
def test_preview_mode_without_session_force_closes(self):
40+
"""A 'preview' panel needs file content from a session — close it
41+
when there's no session."""
42+
idx = BOOT_JS.find("function syncWorkspacePanelState()")
43+
body = BOOT_JS[idx:idx + 800]
44+
assert "_workspacePanelMode==='preview'" in body, (
45+
"syncWorkspacePanelState must check _workspacePanelMode==='preview' "
46+
"before force-closing on no-session boot"
47+
)
48+
assert "_setWorkspacePanelMode('closed')" in body, (
49+
"syncWorkspacePanelState still must close 'preview' mode without a session"
50+
)
51+
52+
def test_browse_mode_calls_sync_ui_instead_of_force_close(self):
53+
"""For 'browse' mode without a session, syncWorkspacePanelUI() should
54+
run so the panel renders its 'no workspace' or default-workspace state
55+
rather than being force-closed."""
56+
idx = BOOT_JS.find("function syncWorkspacePanelState()")
57+
body = BOOT_JS[idx:idx + 800]
58+
# The else branch (browse / closed mode without session) calls UI sync
59+
assert "syncWorkspacePanelUI()" in body, (
60+
"syncWorkspacePanelState must call syncWorkspacePanelUI() in the "
61+
"no-session, non-preview branch so 'browse' mode is preserved"
62+
)
63+
64+
65+
# ── 2. Both boot paths restore panelPref before sync ────────────────────────
66+
67+
68+
class TestBootPathsRestorePanelPref:
69+
PREF_PATTERN = "hermes-webui-workspace-panel-pref"
70+
71+
def test_ephemeral_path_restores_panel_pref(self):
72+
"""The empty-session guard (#1182) must read panelPref before
73+
calling syncWorkspacePanelState()."""
74+
# Find the ephemeral guard — it's marked by message_count===0 check
75+
eph_idx = BOOT_JS.find("(S.session.message_count||0) === 0")
76+
assert eph_idx > 0, "Empty-session guard not found in boot IIFE"
77+
# The next syncWorkspacePanelState() call after this point is in the ephemeral path
78+
sync_idx = BOOT_JS.find("syncWorkspacePanelState()", eph_idx)
79+
assert sync_idx > 0, "syncWorkspacePanelState call not found in ephemeral path"
80+
# panelPref must be read between the guard and the sync call
81+
block = BOOT_JS[eph_idx:sync_idx]
82+
assert self.PREF_PATTERN in block, (
83+
"Ephemeral-session boot path must read 'hermes-webui-workspace-panel-pref' "
84+
"from localStorage before calling syncWorkspacePanelState()"
85+
)
86+
assert "_workspacePanelMode='browse'" in block or "_workspacePanelMode = 'browse'" in block, (
87+
"Ephemeral-session path must set _workspacePanelMode='browse' "
88+
"when the pref is 'open'"
89+
)
90+
91+
def test_no_session_path_restores_panel_pref(self):
92+
"""The fresh-load (no localStorage session) path must read panelPref
93+
before calling syncWorkspacePanelState()."""
94+
# Find the comment marker that precedes the no-session path
95+
marker = "no saved session"
96+
m_idx = BOOT_JS.find(marker)
97+
assert m_idx > 0, "no-saved-session path not found"
98+
# syncWorkspacePanelState should appear shortly after
99+
sync_idx = BOOT_JS.find("syncWorkspacePanelState()", m_idx)
100+
assert sync_idx > 0, "syncWorkspacePanelState() not found after no-saved-session marker"
101+
block = BOOT_JS[m_idx:sync_idx]
102+
assert self.PREF_PATTERN in block, (
103+
"No-saved-session boot path must read 'hermes-webui-workspace-panel-pref' "
104+
"before calling syncWorkspacePanelState()"
105+
)
106+
assert "_workspacePanelMode='browse'" in block or "_workspacePanelMode = 'browse'" in block, (
107+
"No-saved-session path must set _workspacePanelMode='browse' "
108+
"when the pref is 'open'"
109+
)
110+
111+
112+
# ── 3. Toggle button stays enabled when profile default workspace exists ────
113+
114+
115+
class TestToggleStaysEnabledWithProfileWorkspace:
116+
def test_can_browse_includes_profile_default_workspace(self):
117+
"""The toggle button's enabled state (canBrowse) must be true when
118+
S._profileDefaultWorkspace is set, even with no active session."""
119+
idx = BOOT_JS.find("const canBrowse=")
120+
assert idx > 0, "canBrowse declaration not found in syncWorkspacePanelUI"
121+
line = BOOT_JS[idx:idx + 200].split("\n", 1)[0]
122+
assert "_profileDefaultWorkspace" in line, (
123+
"canBrowse must include S._profileDefaultWorkspace so the toggle "
124+
"button stays enabled when a profile workspace is configured"
125+
)
126+
127+
def test_open_workspace_panel_allows_browse_with_profile_workspace(self):
128+
"""openWorkspacePanel('browse') must not return early when
129+
S._profileDefaultWorkspace is set, otherwise clicking the toggle
130+
won't open the panel even though canBrowse said it should."""
131+
idx = BOOT_JS.find("function openWorkspacePanel(")
132+
body = BOOT_JS[idx:idx + 600]
133+
# The early-return guard should include the profile-workspace check
134+
assert "_profileDefaultWorkspace" in body, (
135+
"openWorkspacePanel must include S._profileDefaultWorkspace in its "
136+
"early-return guard so users can open the panel via the toggle "
137+
"button when a profile workspace is configured"
138+
)

0 commit comments

Comments
 (0)