Closes #631. Closes #804. Bug A (thinking card below answer / double render / stuck cursor): trailing rAF after 'done' inserted a duplicate live-turn wrapper into already-settled DOM. Fixed via _streamFinalized flag + cancelAnimationFrame in all terminal handlers (done/apperror/cancel/_handleStreamError) + _scheduleRender guard. All three reported symptoms were the same root cause. Bug B (accumulator reset): original fix reset assistantText/reasoningText inside _wireSSE on reconnect. Reverted — server uses one-shot queue.Queue(), no replay on reconnect, reset would wipe valid pre-drop content causing data loss. Bug A fix alone resolves all symptoms. #804 (blank page workspace): syncWorkspaceDisplays uses S._profileDefaultWorkspace as fallback; workspace chip enabled when hasWorkspace (not hasSession); promptNewFile/promptNewFolder/ switchToWorkspace/promptWorkspacePath auto-create session on blank page; boot.js hydrates _profileDefaultWorkspace from /api/settings before any session exists. Opus max-effort review + Nathan independent review + full browser QA. 1765/1765 tests.
153 lines
6.9 KiB
Python
153 lines
6.9 KiB
Python
"""Tests for #804 — blank new-chat page loses default workspace binding
|
|
|
|
Fixes:
|
|
- syncWorkspaceDisplays() uses S._profileDefaultWorkspace as fallback when no session
|
|
- composerChip.disabled uses hasWorkspace (not hasSession) so chip is enabled on blank page
|
|
- boot.js reads default_workspace from /api/settings and sets S._profileDefaultWorkspace
|
|
- promptNewFile/promptNewFolder auto-create a session bound to default workspace
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
|
|
|
|
def read(rel):
|
|
return (REPO / rel).read_text(encoding='utf-8')
|
|
|
|
|
|
class TestSyncWorkspaceDisplaysFallback:
|
|
"""syncWorkspaceDisplays must show default workspace when no session."""
|
|
|
|
def test_uses_profile_default_workspace_as_fallback(self):
|
|
src = read('static/panels.js')
|
|
m = re.search(r'function syncWorkspaceDisplays\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m, "syncWorkspaceDisplays not found"
|
|
fn = m.group(0)
|
|
assert '_profileDefaultWorkspace' in fn, (
|
|
"syncWorkspaceDisplays must read S._profileDefaultWorkspace as fallback "
|
|
"when no active session is present"
|
|
)
|
|
|
|
def test_has_workspace_not_has_session_for_chip_disable(self):
|
|
src = read('static/panels.js')
|
|
m = re.search(r'function syncWorkspaceDisplays\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m
|
|
fn = m.group(0)
|
|
# composerChip.disabled must use hasWorkspace, not hasSession
|
|
assert 'composerChip.disabled=!hasWorkspace' in fn or \
|
|
'composerChip.disabled = !hasWorkspace' in fn, (
|
|
"composerChip.disabled must use !hasWorkspace (not !hasSession) so the chip "
|
|
"is enabled on the blank new-chat page when a default workspace is configured"
|
|
)
|
|
assert 'composerChip.disabled=!hasSession' not in fn, (
|
|
"composerChip.disabled must not use !hasSession — this was the regression"
|
|
)
|
|
|
|
|
|
class TestBootJsProfileDefaultWorkspace:
|
|
"""boot.js must read default_workspace from /api/settings into S._profileDefaultWorkspace."""
|
|
|
|
def test_boot_reads_default_workspace_from_settings(self):
|
|
src = read('static/boot.js')
|
|
assert '_profileDefaultWorkspace' in src, (
|
|
"boot.js must set S._profileDefaultWorkspace from the /api/settings "
|
|
"default_workspace field so it is available before any session is created"
|
|
)
|
|
|
|
def test_boot_sets_profile_default_workspace_in_settings_block(self):
|
|
"""The settings block (lines ~758-800 in boot.js) must set
|
|
S._profileDefaultWorkspace from the /api/settings response."""
|
|
src = read('static/boot.js')
|
|
# Find the settings fetch and the _profileDefaultWorkspace assignment
|
|
# and confirm both are in the same settings-read block (within ~50 lines)
|
|
ws_idx = src.find('_profileDefaultWorkspace')
|
|
settings_idx = src.find("await api('/api/settings')")
|
|
assert ws_idx != -1, "_profileDefaultWorkspace not found in boot.js"
|
|
assert settings_idx != -1, "await api('/api/settings') not found in boot.js"
|
|
# Both must be within 300 chars of each other (same block)
|
|
assert abs(ws_idx - settings_idx) < 1000, (
|
|
"S._profileDefaultWorkspace must be set in the same settings-fetch block"
|
|
)
|
|
|
|
|
|
class TestPromptNewFileNoSession:
|
|
"""promptNewFile/promptNewFolder must auto-create a session on blank page."""
|
|
|
|
def test_prompt_new_file_auto_creates_session(self):
|
|
src = read('static/ui.js')
|
|
m = re.search(r'async function promptNewFile\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m, "promptNewFile not found"
|
|
fn = m.group(0)
|
|
# Must have auto-create path (not just early return when no session)
|
|
assert '_profileDefaultWorkspace' in fn, (
|
|
"promptNewFile must read S._profileDefaultWorkspace to auto-create "
|
|
"a session when called on the blank new-chat page"
|
|
)
|
|
assert 'session/new' in fn, (
|
|
"promptNewFile must call /api/session/new to create a session "
|
|
"bound to the default workspace when S.session is null"
|
|
)
|
|
|
|
def test_prompt_new_folder_auto_creates_session(self):
|
|
src = read('static/ui.js')
|
|
m = re.search(r'async function promptNewFolder\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m, "promptNewFolder not found"
|
|
fn = m.group(0)
|
|
assert '_profileDefaultWorkspace' in fn, (
|
|
"promptNewFolder must read S._profileDefaultWorkspace for auto-create path"
|
|
)
|
|
assert 'session/new' in fn, (
|
|
"promptNewFolder must call /api/session/new to create session on blank page"
|
|
)
|
|
|
|
def test_prompt_new_file_still_returns_early_without_default(self):
|
|
"""If no default workspace, the function should return early (not crash)."""
|
|
src = read('static/ui.js')
|
|
m = re.search(r'async function promptNewFile\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m
|
|
fn = m.group(0)
|
|
# Must have a guard for empty workspace
|
|
assert "if(!ws) return" in fn or "if(!ws)return" in fn, (
|
|
"promptNewFile must return early if no default workspace is configured"
|
|
)
|
|
|
|
|
|
class TestWorkspaceSwitcherBlankPage:
|
|
"""Opus review Q6: workspace switcher dropdown must not silently fail on blank page."""
|
|
|
|
def test_switch_to_workspace_auto_creates_session(self):
|
|
src = read('static/panels.js')
|
|
m = re.search(r'async function switchToWorkspace\(.*?\n\}', src, re.DOTALL)
|
|
assert m, "switchToWorkspace not found"
|
|
fn = m.group(0)
|
|
assert '_profileDefaultWorkspace' in fn or 'session/new' in fn, (
|
|
"switchToWorkspace must auto-create session on blank page (Opus Q6 fix)"
|
|
)
|
|
assert 'session/new' in fn, (
|
|
"switchToWorkspace must call /api/session/new when S.session is null"
|
|
)
|
|
|
|
def test_prompt_workspace_path_auto_creates_session(self):
|
|
src = read('static/panels.js')
|
|
m = re.search(r'async function promptWorkspacePath\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m, "promptWorkspacePath not found"
|
|
fn = m.group(0)
|
|
assert 'session/new' in fn, (
|
|
"promptWorkspacePath must call /api/session/new when S.session is null"
|
|
)
|
|
|
|
def test_sync_workspace_displays_dropdown_close_uses_has_workspace(self):
|
|
src = read('static/panels.js')
|
|
m = re.search(r'function syncWorkspaceDisplays\(\)\{.*?\n\}', src, re.DOTALL)
|
|
assert m, "syncWorkspaceDisplays not found"
|
|
fn = m.group(0)
|
|
# Line 555: dropdown force-close must use hasWorkspace, not hasSession
|
|
assert '!hasWorkspace && composerDropdown' in fn or '!hasWorkspace&&composerDropdown' in fn, (
|
|
"syncWorkspaceDisplays must use !hasWorkspace (not !hasSession) to decide "
|
|
"whether to force-close the dropdown (Opus Q6 fix)"
|
|
)
|
|
assert '!hasSession && composerDropdown' not in fn, (
|
|
"Regression guard: !hasSession for dropdown close must be removed"
|
|
)
|