🔧 Initial dev copy from live

This commit is contained in:
Rose
2026-04-20 10:43:30 +02:00
commit 96977b576a
284 changed files with 95780 additions and 0 deletions

300
tests/test_sprint34.py Normal file
View File

@@ -0,0 +1,300 @@
"""
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
# ── 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
# ── 13. _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
# ── 45. _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}"
)
# ── 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()
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()
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"