HERMES-GIELIE/tests/test_sprint34.py

301 lines
14 KiB
Python
Raw Normal View History

fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
"""
Sprint 34 Tests: OAuth provider support in onboarding (issues #303, #304).
Covers:
1. _provider_oauth_authenticated() returns True for known OAuth providers
with valid tokens in auth.json
2. _provider_oauth_authenticated() returns False when auth.json is absent,
empty, or has no token data
3. _provider_oauth_authenticated() returns False for unknown/API-key providers
4. _status_from_runtime() marks copilot/openai-codex as provider_ready when
credentials exist
5. _status_from_runtime() gives a helpful "hermes auth" note (not "API key")
for OAuth providers that have no credentials yet
6. API route /api/onboarding/status reflects OAuth-ready state
"""
import json
import pathlib
import tempfile
import unittest.mock
import pytest
REPO = pathlib.Path(__file__).parent.parent
from tests._pytest_port import BASE
fix: recognize OAuth providers as ready in onboarding (closes #303, #304) * fix: recognize OAuth providers as ready in onboarding (closes #303, #304) OAuth-authenticated providers (GitHub Copilot, OpenAI Codex, Nous Portal, Qwen OAuth) were incorrectly blocked by the first-run onboarding wizard because _status_from_runtime() only treated providers in _SUPPORTED_PROVIDER_SETUPS as valid, and _provider_api_key_present() only checked for plain API keys. Changes in api/onboarding.py: - Add _provider_oauth_authenticated(provider, hermes_home): checks hermes_cli.auth.get_auth_status() first (authoritative), then falls back to parsing ~/.hermes/auth.json directly for the known OAuth provider IDs (openai-codex, copilot, copilot-acp, qwen-oauth, nous). - _status_from_runtime(): add else branch for providers not in _SUPPORTED_PROVIDER_SETUPS; calls _provider_oauth_authenticated() so copilot/openai-codex users with valid credentials get provider_ready=True. - Fix misleading 'API key' wording in provider_incomplete note for OAuth providers; now says 'Run hermes auth or hermes model to complete setup.' 19 new tests in tests/test_sprint34.py covering all branches. * fix: mock _HERMES_FOUND in _status_from_runtime tests 5 tests in TestStatusFromRuntimeOAuth failed because _status_from_runtime() short-circuits to 'agent_unavailable' when _HERMES_FOUND is False. The tests passed imports_ok=True but _HERMES_FOUND is a separate module-level flag. Fixed: _call() helper now mocks _HERMES_FOUND=True with restore in finally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:37:38 +00:00
# ── Helpers ──────────────────────────────────────────────────────────────────
def _make_auth_json(provider_id: str, tokens: dict, tmp_dir: pathlib.Path) -> pathlib.Path:
"""Write an auth.json with the given tokens for provider_id into tmp_dir."""
store = {"providers": {provider_id: tokens}}
auth_path = tmp_dir / "auth.json"
auth_path.write_text(json.dumps(store), encoding="utf-8")
return auth_path
# ── 1–3. _provider_oauth_authenticated unit tests ────────────────────────────
class TestProviderOAuthAuthenticated:
"""Unit tests for the new _provider_oauth_authenticated() helper."""
def _call(self, provider: str, hermes_home: pathlib.Path) -> bool:
# Import fresh so we don't get a stale module reference
from api.onboarding import _provider_oauth_authenticated
return _provider_oauth_authenticated(provider, hermes_home)
def test_returns_false_when_auth_json_absent(self, tmp_path):
"""No auth.json -> not authenticated."""
assert self._call("openai-codex", tmp_path) is False
def test_openai_codex_with_access_token(self, tmp_path):
"""openai-codex with a valid access_token -> authenticated."""
_make_auth_json(
"openai-codex",
{"access_token": "ey.test.token", "refresh_token": "ref123"},
tmp_path,
)
assert self._call("openai-codex", tmp_path) is True
def test_openai_codex_with_refresh_token_only(self, tmp_path):
"""openai-codex with only a refresh_token -> still authenticated."""
_make_auth_json(
"openai-codex",
{"access_token": "", "refresh_token": "ref_only_token"},
tmp_path,
)
assert self._call("openai-codex", tmp_path) is True
def test_copilot_with_api_key(self, tmp_path):
"""copilot with an api_key (GitHub token) -> authenticated."""
_make_auth_json("copilot", {"api_key": "ghu_test_token_123"}, tmp_path)
assert self._call("copilot", tmp_path) is True
def test_empty_tokens_returns_false(self, tmp_path):
"""All token fields empty -> not authenticated."""
_make_auth_json(
"openai-codex",
{"access_token": "", "refresh_token": "", "api_key": ""},
tmp_path,
)
assert self._call("openai-codex", tmp_path) is False
def test_missing_provider_key_in_auth_json(self, tmp_path):
"""auth.json present but provider key absent -> not authenticated."""
store = {"providers": {"some-other-provider": {"access_token": "tok"}}}
(tmp_path / "auth.json").write_text(json.dumps(store), encoding="utf-8")
assert self._call("openai-codex", tmp_path) is False
def test_unknown_provider_not_in_oauth_list(self, tmp_path):
"""A provider that is not a known OAuth provider -> always False."""
_make_auth_json("some-random-provider", {"access_token": "tok"}, tmp_path)
assert self._call("some-random-provider", tmp_path) is False
def test_nous_provider_recognized(self, tmp_path):
"""nous is in the known OAuth set."""
_make_auth_json("nous", {"access_token": "nous_tok"}, tmp_path)
assert self._call("nous", tmp_path) is True
def test_qwen_oauth_provider_recognized(self, tmp_path):
"""qwen-oauth is in the known OAuth set."""
_make_auth_json("qwen-oauth", {"access_token": "qwen_tok"}, tmp_path)
assert self._call("qwen-oauth", tmp_path) is True
def test_empty_provider_string_returns_false(self, tmp_path):
"""Empty provider string -> False, no crash."""
assert self._call("", tmp_path) is False
assert self._call(" ", tmp_path) is False
# ── 4–5. _status_from_runtime integration ────────────────────────────────────
class TestStatusFromRuntimeOAuth:
"""_status_from_runtime should treat OAuth providers with tokens as ready."""
def _call(self, provider: str, model: str, hermes_home: pathlib.Path) -> dict:
from api.onboarding import _status_from_runtime
import api.onboarding as _ob
orig_home = _ob._get_active_hermes_home
orig_found = _ob._HERMES_FOUND
_ob._get_active_hermes_home = lambda: hermes_home
# Simulate hermes-agent being available so we reach the provider logic
# (without this, _status_from_runtime short-circuits to agent_unavailable)
_ob._HERMES_FOUND = True
try:
cfg = {"model": {"provider": provider, "default": model}}
return _status_from_runtime(cfg, True)
finally:
_ob._get_active_hermes_home = orig_home
_ob._HERMES_FOUND = orig_found
def test_copilot_ready_when_api_key_in_auth_json(self, tmp_path):
"""copilot configured + api_key in auth.json -> provider_ready True."""
_make_auth_json("copilot", {"api_key": "ghu_abc123"}, tmp_path)
result = self._call("copilot", "gpt-5.4", tmp_path)
assert result["provider_configured"] is True
assert result["provider_ready"] is True
assert result["setup_state"] == "ready"
def test_openai_codex_ready_when_token_in_auth_json(self, tmp_path):
"""openai-codex configured + access_token -> provider_ready True."""
_make_auth_json(
"openai-codex",
{"access_token": "ey.test", "refresh_token": "ref"},
tmp_path,
)
result = self._call("openai-codex", "codex-mini-latest", tmp_path)
assert result["provider_configured"] is True
assert result["provider_ready"] is True
assert result["setup_state"] == "ready"
def test_copilot_not_ready_without_credentials(self, tmp_path):
"""copilot configured but no credentials -> provider_ready False.
We mock hermes_cli.auth to be unavailable so the function falls through
to the auth.json path. With no auth.json the result must be False.
"""
import unittest.mock
# Prevent the hermes_cli fast path from finding real credentials
with unittest.mock.patch(
"api.onboarding._provider_oauth_authenticated",
return_value=False,
):
result = self._call("copilot", "gpt-5.4", tmp_path)
assert result["provider_configured"] is True
assert result["provider_ready"] is False
assert result["setup_state"] == "provider_incomplete"
def test_oauth_incomplete_note_mentions_hermes_auth(self, tmp_path):
"""When OAuth provider is incomplete, note should mention hermes auth/model."""
result = self._call("openai-codex", "codex-mini-latest", tmp_path)
note = result["provider_note"]
assert "hermes auth" in note or "hermes model" in note, (
f"Expected 'hermes auth' or 'hermes model' in note, got: {note!r}"
)
def test_oauth_incomplete_note_does_not_say_api_key(self, tmp_path):
"""OAuth provider incomplete note must not say 'API key' — that's misleading."""
result = self._call("copilot", "gpt-5.4", tmp_path)
note = result["provider_note"]
assert "API key" not in note, (
f"Note misleadingly mentions 'API key' for OAuth provider: {note!r}"
)
def test_standard_provider_incomplete_note_still_says_api_key(self, tmp_path):
"""For a standard API-key provider (openrouter), note should still say API key."""
# openrouter with no .env
result = self._call("openrouter", "anthropic/claude-sonnet-4.6", tmp_path)
assert result["provider_ready"] is False
note = result["provider_note"]
assert "API key" in note, (
f"Expected 'API key' in note for openrouter, got: {note!r}"
)
# ── 6. API endpoint reflects OAuth-ready state ───────────────────────────────
class TestOnboardingStatusApiOAuth:
"""
The /api/onboarding/status endpoint should report provider_ready=True
when an OAuth provider is configured and has valid credentials.
"""
def test_status_endpoint_returns_200(self):
import urllib.request
with urllib.request.urlopen(BASE + "/api/onboarding/status", timeout=10) as r:
assert r.status == 200
data = json.loads(r.read())
assert "system" in data
assert "provider_ready" in data["system"]
def test_onboarding_status_has_chat_ready_field(self):
import urllib.request
with urllib.request.urlopen(BASE + "/api/onboarding/status", timeout=10) as r:
data = json.loads(r.read())
assert "chat_ready" in data["system"]
def test_status_setup_state_valid_values(self):
"""setup_state must be one of the known string values."""
import urllib.request
with urllib.request.urlopen(BASE + "/api/onboarding/status", timeout=10) as r:
data = json.loads(r.read())
valid = {"ready", "provider_incomplete", "needs_provider", "agent_unavailable"}
assert data["system"]["setup_state"] in valid, (
f"Unexpected setup_state: {data['system']['setup_state']!r}"
)
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
# ── Control Center: section reset on close ─────────────────────────────────
def test_control_center_resets_active_section_on_close():
"""Closing the control center must reset _settingsSection to 'conversation'."""
src = open(pathlib.Path(__file__).parent.parent / 'static' / 'panels.js').read()
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
assert '_settingsSection' in src, '_settingsSection state variable missing from panels.js'
assert "_settingsSection = 'conversation'" in src or "_settingsSection='conversation'" in src, \
'Control center does not reset section to conversation on close'
def test_control_center_tab_highlight_on_open():
"""Opening the control center must use settings-tabs for section navigation."""
css = open(pathlib.Path(__file__).parent.parent / 'static' / 'style.css').read()
feat: composer-centric UI refresh + Hermes Control Center (v0.50.0, closes #242) * Polish workspace panel behavior and app dialogs * Replace remaining emoji UI glyphs with Lucide icons * Redesign composer footer around model and context controls Move the model selector into the composer footer, replace the linear context pill with a compact circular badge plus tooltip, and remove the redundant topbar model pill. Design credit and inspiration: Theo / T3 Code. Reference implementation: https://github.com/pingdotgg/t3code/ * Remove obsolete activity bar Drop the old activity bar, keep turn-scoped state in the composer footer, and route remaining non-chat status messages through toasts. This leaves live tool cards and the message timeline as the primary progress UI, with the composer owning stop/cancel and brief turn status. * Move workspace and model switching into composer footer * Move profile switching into composer footer * Refactor Hermes control center UI * Redesign control center settings modal layout Widen the modal to 860px, simplify the tab list to icon+label rows, stretch the tab column's divider to full height, lock the panel to a fixed height so switching tabs no longer resizes the outer shell, and always open on the Conversation tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Put session item actions in a dropdown * Use Hermes mark in sidebar control button * Reset control center section on close * Drop session-item left border indicator Remove the left-border accent used for active, CLI, and project rows — each state already has a dedicated cue (gold fill, cli badge, project dot), so the border was redundant. Fully round the row, add 2px bottom spacing between rows, and strip the matching JS/CSS overrides. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Increase session search input vertical padding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Normalise odd pixel values across UI Snap padding, gap, and border-radius values to the 2/4/6/8/10/12 grid across composer chips, sidebar panels, cron list, settings, approval buttons, dropdowns, and inline message edit — eliminating the 7/9/11px drift that was making sibling elements feel subtly misaligned. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add missing #btnMobileFiles button and .mobile-files-btn CSS (for mobile QA suite) The mobile layout regression suite (test_mobile_layout.py) requires: - #btnMobileFiles onclick=toggleMobileFiles() in topbar chips - .mobile-files-btn CSS rules for responsive show/hide at 640/900px breakpoints Also adds max-width guard to .profile-dropdown to prevent clipping at narrow viewports. * Improve composer footer mobile responsiveness and UX - Collapse composer chips to icon-only at <=400px viewports - Add model chip icon (CPU) so it remains tappable when labels are hidden - Show send button always (disabled state when empty, hidden during streaming) - Show context usage indicator on session load, not just after streaming - Add cancel status fallback timeout to prevent stale "Cancelling..." text - Update tests to match new send button and busy state behavior * Fix duplicate files button and broken workspace close on mobile Remove redundant #btnMobileFiles button that duplicated #btnWorkspacePanelToggle in the mobile topbar. Fix workspace panel close button calling undefined closeMobileFiles() — now calls closeWorkspacePanel(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix model chip icon vertical alignment in composer footer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix workspace toggle button hidden on desktop by conflicting CSS class Remove mobile-files-btn class from #btnWorkspacePanelToggle — its display:none!important rule was overriding workspace-toggle-btn visibility on non-mobile viewports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix session actions dots button inaccessible on mobile sidebar Always show the session actions trigger on mobile (no hover state on touch devices) and restore right padding so text truncates with ellipsis before the dots icon. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer footer manage links not opening sidebar panel The "Manage profiles" and "Manage workspaces" links in the composer footer dropdowns called switchPanel() which only changes the active panel content but doesn't open the sidebar. Replaced with mobileSwitchPanel() which also opens the sidebar so the panel is actually visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Widen icon-only composer chips breakpoint from 400px to 768px Move the icon-only chip styling up into the existing max-width:768px media query so chips collapse to icon-only on tablets too, preventing composer footer overflow on mid-size screens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix composer-left vertical scrollbar by setting overflow-y:hidden When overflow-x is set to auto, the CSS spec implicitly changes overflow-y from visible to auto, allowing a vertical scrollbar to appear from slight chip padding/border overflow. Explicitly set overflow-y:hidden to prevent this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve rebase conflicts and fix control center test assertions - Resolved 4 conflicts during rebase onto master (workspace.js, boot.js, index.html, test_sprint34.py) - Fixed test_sprint34.py: _controlSection -> _settingsSection, cc-tab -> settings-tabs (matching actual implementation) - Fixed quoting syntax error in test assertion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update version badge in System tab to v0.49.4 * docs: update README and CHANGELOG for v0.50.0 UI refresh, bump version badge --------- Co-authored-by: Aron Prins <pwf.aron@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
2026-04-12 18:55:40 +00:00
assert 'settings-tabs' in css, 'settings-tabs CSS class for control center tabs missing from style.css'
# ── apply_onboarding_setup: unsupported/OAuth providers complete gracefully ──
class TestApplyOnboardingSetupUnsupportedProvider:
"""PR #323 / Issue #322: apply_onboarding_setup must not raise ValueError for
providers already configured via CLI (openai-codex, copilot, nous, etc.).
Instead it marks onboarding complete and returns current status.
"""
def _call(self, provider: str) -> dict:
import sys, pathlib, unittest.mock, tempfile, os
repo = pathlib.Path(__file__).parent.parent
if str(repo) not in sys.path:
sys.path.insert(0, str(repo))
from api.onboarding import apply_onboarding_setup
with tempfile.TemporaryDirectory() as tmp:
with unittest.mock.patch("api.onboarding._get_active_hermes_home",
return_value=pathlib.Path(tmp)), \
unittest.mock.patch("api.onboarding._get_config_path",
return_value=pathlib.Path(tmp) / "config.yaml"), \
unittest.mock.patch("api.onboarding.save_settings") as mock_save, \
unittest.mock.patch("api.onboarding.get_onboarding_status",
return_value={"completed": True, "system": {}}):
result = apply_onboarding_setup({"provider": provider, "model": "", "api_key": ""})
return result, mock_save
def test_openai_codex_does_not_raise(self):
"""apply_onboarding_setup with openai-codex must not raise ValueError."""
result, _ = self._call("openai-codex")
assert result is not None
def test_copilot_does_not_raise(self):
"""apply_onboarding_setup with copilot must not raise ValueError."""
result, _ = self._call("copilot")
assert result is not None
def test_nous_does_not_raise(self):
"""apply_onboarding_setup with nous must not raise ValueError."""
result, _ = self._call("nous")
assert result is not None
def test_unsupported_provider_marks_onboarding_complete(self):
"""apply_onboarding_setup with an unsupported provider must save onboarding_completed=True."""
_, mock_save = self._call("openai-codex")
calls = [str(c) for c in mock_save.call_args_list]
assert any("onboarding_completed" in c for c in calls), \
"save_settings must be called with onboarding_completed=True for unsupported providers"
def test_unsupported_provider_returns_status_dict(self):
"""apply_onboarding_setup with an unsupported provider must return a status dict (not raise)."""
result, _ = self._call("openai-codex")
assert isinstance(result, dict), \
"apply_onboarding_setup must return a dict for unsupported providers, not raise"