fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
"""
|
2026-05-21 13:30:41 +00:00
|
|
|
Tests for profile-switch UX improvements.
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
|
2026-05-21 13:30:41 +00:00
|
|
|
Covered behavior:
|
|
|
|
|
- switchToProfile() shows a spinner during the async switch and reverts on error.
|
|
|
|
|
- Non-visible refresh work runs after the visible switch completes.
|
|
|
|
|
- Session-list refreshes animate rows with row-level FLIP motion.
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
"""
|
|
|
|
|
import re
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProfileSwitchSpinner:
|
|
|
|
|
"""Static-analysis tests for the spinner loading indicator."""
|
|
|
|
|
|
|
|
|
|
JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
def _get_switch_fn(self):
|
|
|
|
|
idx = self.JS.find("async function switchToProfile(name) {")
|
|
|
|
|
assert idx != -1, "switchToProfile not found in panels.js"
|
|
|
|
|
depth = 0
|
|
|
|
|
for i, ch in enumerate(self.JS[idx:], idx):
|
|
|
|
|
if ch == "{": depth += 1
|
|
|
|
|
elif ch == "}":
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0:
|
|
|
|
|
return self.JS[idx: i + 1]
|
|
|
|
|
raise AssertionError("Could not extract switchToProfile")
|
|
|
|
|
|
|
|
|
|
def test_switching_class_added_on_start(self):
|
|
|
|
|
"""The switching CSS class must be added before any awaits."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
assert "classList.add('switching')" in fn, (
|
|
|
|
|
"switchToProfile() does not add 'switching' CSS class to the chip."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_switching_class_removed_in_finally(self):
|
|
|
|
|
"""The switching class must be removed in a finally block."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
finally_idx = fn.find("} finally {")
|
|
|
|
|
assert finally_idx != -1, "switchToProfile() has no finally block."
|
|
|
|
|
assert "classList.remove('switching')" in fn[finally_idx:], (
|
|
|
|
|
"The finally block does not remove 'switching' class."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_optimistic_name_set_before_api_call(self):
|
|
|
|
|
"""Chip label must be updated to new name before the API call."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
api_call_idx = fn.find("await api('/api/profile/switch'")
|
|
|
|
|
opt_name_idx = fn.find("_chipLabel.textContent = name")
|
|
|
|
|
assert opt_name_idx != -1, "No optimistic name update found."
|
|
|
|
|
assert opt_name_idx < api_call_idx, (
|
|
|
|
|
"Optimistic name update must happen BEFORE the API call."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_chip_disabled_during_switch(self):
|
|
|
|
|
"""Chip must be disabled to prevent double-clicks."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
assert "_chip.disabled = true" in fn, (
|
|
|
|
|
"switchToProfile() does not disable the chip."
|
|
|
|
|
)
|
|
|
|
|
finally_idx = fn.find("} finally {")
|
|
|
|
|
assert finally_idx != -1
|
|
|
|
|
assert "_chip.disabled = false" in fn[finally_idx:], (
|
|
|
|
|
"The finally block does not re-enable the chip."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_error_reverts_chip_label_to_previous_name(self):
|
|
|
|
|
"""On error, the chip label must revert to the previous name."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
catch_idx = fn.find("} catch (e) {")
|
|
|
|
|
assert catch_idx != -1
|
|
|
|
|
assert "_prevProfileName" in fn[catch_idx:], (
|
|
|
|
|
"The catch block does not restore _prevProfileName."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestParallelizedFetches:
|
2026-05-20 12:55:17 +00:00
|
|
|
"""Verify that background refresh work does not block visible profile switching."""
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
|
|
|
|
|
JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
def _get_switch_fn(self):
|
|
|
|
|
idx = self.JS.find("async function switchToProfile(name) {")
|
|
|
|
|
assert idx != -1
|
|
|
|
|
depth = 0
|
|
|
|
|
for i, ch in enumerate(self.JS[idx:], idx):
|
|
|
|
|
if ch == "{": depth += 1
|
|
|
|
|
elif ch == "}":
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0:
|
|
|
|
|
return self.JS[idx: i + 1]
|
|
|
|
|
raise AssertionError("Could not extract switchToProfile")
|
|
|
|
|
|
2026-05-20 12:55:17 +00:00
|
|
|
def test_workspace_refresh_in_background_and_model_catalog_lazy(self):
|
|
|
|
|
"""Workspace refresh should run behind completion; model catalog waits for picker open."""
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
fn = self._get_switch_fn()
|
2026-05-20 12:55:17 +00:00
|
|
|
assert "_refreshProfileSwitchBackground(_switchGen)" in fn, (
|
|
|
|
|
"switchToProfile() must schedule non-visible refreshes after the switch."
|
|
|
|
|
)
|
|
|
|
|
assert "window._modelDropdownReady=null" in self.JS
|
|
|
|
|
assert "await Promise.all([populateModelDropdown(), loadWorkspaceList()])" not in fn, (
|
|
|
|
|
"Profile switching still awaits full model/workspace catalog refreshes."
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_no_sequential_await_pattern(self):
|
|
|
|
|
"""The old sequential await pattern must be gone."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
sequential = re.search(
|
|
|
|
|
r"await populateModelDropdown\(\)\s*;\s*\n\s*await loadWorkspaceList",
|
|
|
|
|
fn
|
|
|
|
|
)
|
|
|
|
|
assert not sequential, (
|
|
|
|
|
"Old sequential await pattern still present — both fetches would run twice."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_apply_steps_after_promise_all(self):
|
2026-05-20 12:55:17 +00:00
|
|
|
"""Model defaults must apply before background catalog refresh starts."""
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
fn = self._get_switch_fn()
|
2026-05-20 12:55:17 +00:00
|
|
|
background_idx = fn.find("_refreshProfileSwitchBackground(_switchGen)")
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
apply_model_idx = fn.find("S._pendingProfileModel = modelToUse")
|
|
|
|
|
assert apply_model_idx != -1
|
2026-05-20 12:55:17 +00:00
|
|
|
assert background_idx != -1
|
|
|
|
|
assert apply_model_idx < background_idx, (
|
|
|
|
|
"Model defaults must apply before background refresh starts."
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
)
|
2026-05-20 12:55:17 +00:00
|
|
|
assert "existingDefaultOpt.dataset.provider = providerId" in fn
|
|
|
|
|
|
|
|
|
|
def test_workspace_load_is_awaited_only_when_visible(self):
|
|
|
|
|
"""Profile switches should not duplicate workspace-tree loads."""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
assert "awaitWorkspaceLoad: workspaceVisible" in fn
|
|
|
|
|
sessions_js = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
2026-06-28 10:01:50 +00:00
|
|
|
assert "if(options&&options.awaitWorkspaceLoad){" in sessions_js
|
|
|
|
|
assert "await loadDir('.')" in sessions_js
|
|
|
|
|
assert "typeof _deferWorkspaceRefreshForSession==='function'" in sessions_js
|
|
|
|
|
assert "_deferWorkspaceRefreshForSession(S.session.session_id)" in sessions_js
|
|
|
|
|
assert "const _dirP=loadDir('.')" in sessions_js
|
|
|
|
|
assert "Keep new-chat first paint instant" in sessions_js
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
|
2026-07-03 20:49:51 +00:00
|
|
|
def test_cross_profile_empty_session_is_replaced_before_mutation_or_upload(self):
|
|
|
|
|
"""Switching profile must not keep an old-profile empty session active.
|
|
|
|
|
|
|
|
|
|
Uploads post ``S.session.session_id`` and the backend correctly rejects a
|
|
|
|
|
session that is invisible to the active profile. Therefore a profile
|
|
|
|
|
switch must promote a profile-mismatched, even empty, current session to
|
|
|
|
|
the ``sessionInProgress`` replacement path before any in-place
|
|
|
|
|
``/api/session/update`` or local profile retagging can make the browser
|
|
|
|
|
believe the old session is safe to use.
|
|
|
|
|
"""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
mismatch_idx = fn.find("sessionProfileMatchesTarget")
|
|
|
|
|
assert mismatch_idx != -1, (
|
|
|
|
|
"switchToProfile() must explicitly compare the current session profile "
|
|
|
|
|
"with the target active profile"
|
|
|
|
|
)
|
|
|
|
|
assert "const targetActiveProfile = S.activeProfile || 'default';" in fn
|
|
|
|
|
assert "currentSessionProfile === targetActiveProfile || (currentSessionProfile === 'default' && !!S.activeProfileIsDefault)" in fn, (
|
|
|
|
|
"fallback matching must preserve the renamed-default/root-profile alias semantics"
|
|
|
|
|
)
|
|
|
|
|
promote_idx = fn.find("sessionInProgress = true", mismatch_idx)
|
|
|
|
|
assert promote_idx != -1, (
|
|
|
|
|
"a profile-mismatched current session must force the new-session branch, "
|
|
|
|
|
"even when it has no messages"
|
|
|
|
|
)
|
|
|
|
|
first_in_place_patch = fn.find("if (S.session && !sessionInProgress)")
|
|
|
|
|
first_update = fn.find("await api('/api/session/update'")
|
|
|
|
|
branch_idx = fn.find("if (sessionInProgress)")
|
|
|
|
|
assert -1 not in (first_in_place_patch, first_update, branch_idx)
|
|
|
|
|
assert promote_idx < first_in_place_patch < first_update < branch_idx, (
|
|
|
|
|
"the stale-session promotion must happen before any in-place session "
|
|
|
|
|
"retag/update and before choosing the profile-switch branch"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_profile_switch_opens_session_browser_after_successful_list_refresh(self):
|
|
|
|
|
"""After changing profile, expose the new profile's conversation list.
|
|
|
|
|
|
|
|
|
|
If the user started from an old-profile chat, leaving the old transcript as
|
|
|
|
|
the dominant UI invites sending into a stale session_id. The switch must
|
|
|
|
|
render the new profile's session list, then open the sidebar/mobile drawer
|
|
|
|
|
so the user can choose an existing conversation or click New Chat.
|
|
|
|
|
"""
|
|
|
|
|
fn = self._get_switch_fn()
|
|
|
|
|
assert "function _openProfileSwitchSessionBrowser(" in self.JS
|
|
|
|
|
open_calls = [m.start() for m in re.finditer(r"_openProfileSwitchSessionBrowser\(\)", fn)]
|
|
|
|
|
assert len(open_calls) >= 2, (
|
|
|
|
|
"both profile-switch success branches must expose the new profile's session browser"
|
|
|
|
|
)
|
|
|
|
|
for open_idx in open_calls:
|
|
|
|
|
render_idx = fn.rfind("await renderSessionList();", 0, open_idx)
|
|
|
|
|
assert render_idx != -1 and render_idx < open_idx, (
|
|
|
|
|
"open the sidebar only after the new profile's session list has rendered"
|
|
|
|
|
)
|
|
|
|
|
toast_idx = fn.find("showToast(", open_idx)
|
|
|
|
|
assert toast_idx != -1, "the open call should remain in the success branch before user feedback"
|
|
|
|
|
|
|
|
|
|
def test_profile_switch_session_browser_helper_supports_desktop_and_mobile(self):
|
|
|
|
|
js = self.JS
|
|
|
|
|
start = js.find("function _openProfileSwitchSessionBrowser(")
|
|
|
|
|
assert start != -1
|
|
|
|
|
end = js.find("async function switchToProfile(", start)
|
|
|
|
|
assert end != -1
|
|
|
|
|
helper = js[start:end]
|
|
|
|
|
assert "expandSidebar()" in helper, "desktop profile switches must uncollapse the sidebar"
|
|
|
|
|
assert "mobile-panel-drawer" in helper
|
|
|
|
|
assert "mobile-open" in helper
|
|
|
|
|
assert "mobile-session-page" in helper
|
|
|
|
|
|
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
fix+feat: batch v0.50.236 — OAuth providers fix, profile switch UX, YOLO mode (#1211)
Merges PRs #1208, #1209, #1210 (#1152 rebased):
- fix(providers): OAuth provider cards show correct Configured status in Settings.
get_providers() was discarding has_key=True from _provider_has_key() for OAuth
providers, hiding config.yaml tokens. Also fixed filter excluding all OAuth providers
from the Settings panel. Surfaces auth_error string. (closes #1202)
- ux(profiles): profile chip shows spinner and new name immediately on switch.
Optimistic name update + .switching CSS class + chip disabled + finally cleanup.
populateModelDropdown() and loadWorkspaceList() now parallelized via Promise.all.
- feat: YOLO mode toggle — skip all approvals per session.
/yolo slash command, "Skip all this session" button on approval cards,
amber ⚡ pill indicator in composer footer. Session-scoped, in-memory.
Full i18n: en, ru, es, de, zh, ko, zh-Hant. (closes #467)
Original author: @bergeouss (PR #1152)
Tests: 2837 passed (+50 new tests vs previous release)
QA harness: 20/20 passed + all browser API checks passed
2026-04-28 05:56:12 +00:00
|
|
|
|
|
|
|
|
class TestSpinnerCss:
|
|
|
|
|
"""Verify the spinner CSS class is defined correctly."""
|
|
|
|
|
|
|
|
|
|
CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
def test_switching_class_defined(self):
|
|
|
|
|
assert ".composer-profile-chip.switching" in self.CSS
|
|
|
|
|
|
|
|
|
|
def test_switching_class_has_cursor_wait(self):
|
|
|
|
|
idx = self.CSS.find(".composer-profile-chip.switching")
|
|
|
|
|
assert idx != -1
|
|
|
|
|
block = self.CSS[idx: idx + 200]
|
|
|
|
|
assert "cursor:wait" in block
|
|
|
|
|
|
|
|
|
|
def test_switching_class_has_pointer_events_none(self):
|
|
|
|
|
idx = self.CSS.find(".composer-profile-chip.switching")
|
|
|
|
|
assert idx != -1
|
|
|
|
|
block = self.CSS[idx: idx + 200]
|
|
|
|
|
assert "pointer-events:none" in block
|
2026-05-20 12:55:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProfileSessionListFlip:
|
|
|
|
|
"""Verify session-list refreshes use row-level FLIP motion."""
|
|
|
|
|
|
|
|
|
|
JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
def test_profile_refresh_flips_new_rows(self):
|
|
|
|
|
assert "session-list-flip-enter" in self.JS
|
|
|
|
|
assert "@keyframes sessionListFlipIn" in self.CSS
|
|
|
|
|
|
2026-05-20 15:59:36 +00:00
|
|
|
def test_profile_refresh_captures_before_render_and_plays_after_rows_exist(self):
|
2026-05-21 13:30:41 +00:00
|
|
|
capture = self.JS.index("const flipBefore=animateRefresh?_captureSessionReflowPositions():null;")
|
2026-05-20 15:59:36 +00:00
|
|
|
clear = self.JS.index("list.innerHTML='';", capture)
|
|
|
|
|
row_render = self.JS.index("body.appendChild(_renderOneSession", clear)
|
2026-05-21 13:41:08 +00:00
|
|
|
play = self.JS.index("_playSessionRowsReflowFromPositions(reflowBefore,reflowTimeout,_sessionPrefersReducedMotion);", row_render)
|
2026-05-20 15:59:36 +00:00
|
|
|
|
|
|
|
|
assert capture < clear < row_render < play
|
|
|
|
|
|
2026-05-20 23:18:45 +00:00
|
|
|
def test_profile_refresh_drops_queued_reflow_before_playing_flip(self):
|
2026-05-21 13:41:08 +00:00
|
|
|
start = self.JS.index("// Refresh FLIP and queued archive/delete reflow both drive")
|
fix(sidebar): hoist _sessionAttentionState to fix ReferenceError crash (#3696) + scope-undef prevention gate (#3698)
* fix(sidebar): hoist _sessionAttentionState to top-level scope (#3696)
_sessionAttentionState was declared inside renderSessionListFromCache() and
relied on function hoisting, but the top-level function _sidebarRowHasVisible
Messages (reached via renderSessionListFromCache -> _partitionSidebarSessionRows)
called it bare. Hoisting is scoped to the enclosing function, so every sidebar
cache-render threw 'ReferenceError: _sessionAttentionState is not defined' and
the session list went blank. Regressed in #3672 (v0.51.269) when _sidebarRow
HasVisibleMessages was extracted to top level.
Fix: move _sessionAttentionState to top-level scope (it is pure — only uses its
arg plus the i18n global t), so both the visibility predicate and the nested
per-row renderer can reach it.
Prevention (the durable half): add scripts/scope_undef_gate.py — models the
classic-<script> shared global scope (union of all static files' top-level
symbols) and runs ESLint no-undef per file, flagging a function defined nested
but called from a sibling scope. Wired into CI (.github/workflows/tests.yml lint
job) alongside the existing no-const-assign runtime gate, plus an in-suite test
(test_static_js_scope_undef.py) and a focused structural regression test
(test_issue3696_session_attention_scope.py). RED/GREEN-validated against the
broken tree.
* fix(streaming): thread source param into stale-stream bailout; tighten scope gate
Opus review of #3698 found the new scope_undef_gate's 'source' allowlist entry
was masking a real same-class bug: _bailOutOfTerminalEventsFromStaleStream
(declared inside attachLiveStream, params activeSid/streamId/uploaded/options)
called _closeSource(source) against a 'source' not in its lexical scope. All 5
call sites are inside _wireSSE(source), but JS scope is lexical not dynamic, so
the helper would throw ReferenceError: source is not defined on the stale-stream
terminal-event path (user back in an active session whose old stream finalizes
late).
Fix: thread source as an explicit parameter (declaration + all 5 call sites),
the same make-the-dependency-explicit fix as #3696 — and REMOVE the 'source'
allowlist entry so the gate stays gated against that name (it now passes because
the bug is fixed, not because it's allowlisted). Added the documented
false-negative classes from Opus's review to the gate docstring (name-collision
shadowing, destructuring-regex gap, exposure escape hatches, name-keyed
allowlist) and a focused regression test.
This is the prevention gate catching a real latent bug on its first outing.
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-06 03:49:45 +00:00
|
|
|
# End anchor: the next function declaration after the reflow block. (Was the
|
|
|
|
|
# "// Note: declared after the groups loop" comment on the nested
|
|
|
|
|
# _sessionAttentionState, which #3696 removed when that helper was hoisted to
|
|
|
|
|
# top-level scope — so anchor on the stable _renderOneSession decl instead.)
|
|
|
|
|
end = self.JS.index("function _renderOneSession(", start)
|
2026-05-20 23:18:45 +00:00
|
|
|
block = self.JS[start:end]
|
|
|
|
|
|
2026-05-21 13:41:08 +00:00
|
|
|
assert "const reflowBefore=animateRefresh?flipBefore:_pendingSessionReflowPositions;" in block
|
|
|
|
|
assert "const reflowTimeout=animateRefresh?SESSION_LIST_FLIP_TIMEOUT_MS:SESSION_REFLOW_TIMEOUT_MS;" in block
|
|
|
|
|
assert "_pendingSessionReflowPositions=null;" in block
|
|
|
|
|
assert "_playSessionRowsReflowFromPositions(reflowBefore,reflowTimeout,_sessionPrefersReducedMotion);" in block
|
|
|
|
|
assert block.index("const reflowBefore=animateRefresh?flipBefore:_pendingSessionReflowPositions;") < block.index("const reflowTimeout=animateRefresh?SESSION_LIST_FLIP_TIMEOUT_MS:SESSION_REFLOW_TIMEOUT_MS;")
|
|
|
|
|
assert block.index("const reflowTimeout=animateRefresh?SESSION_LIST_FLIP_TIMEOUT_MS:SESSION_REFLOW_TIMEOUT_MS;") < block.index("_pendingSessionReflowPositions=null;")
|
|
|
|
|
assert block.index("_pendingSessionReflowPositions=null;") < block.index("_playSessionRowsReflowFromPositions(reflowBefore,reflowTimeout,_sessionPrefersReducedMotion);")
|
2026-05-20 23:18:45 +00:00
|
|
|
|
2026-05-20 12:55:17 +00:00
|
|
|
def test_first_non_empty_session_render_is_animated(self):
|
|
|
|
|
assert "_sessionListFirstRenderAnimated" in self.JS
|
|
|
|
|
assert "animateNextSessionListRefresh({enterAll:true});" in self.JS
|
2026-05-20 17:03:37 +00:00
|
|
|
assert "_sessionListFirstRenderAnimated=true;" in self.JS
|
2026-05-20 12:55:17 +00:00
|
|
|
assert "enterAllAnimatedRows" in self.JS
|