forked from Rockachopa/Timmy-time-dashboard
* polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
254 lines
8.8 KiB
Python
254 lines
8.8 KiB
Python
"""Fast E2E tests - all checks in one browser session, under 20 seconds.
|
|
|
|
RUN: SELENIUM_UI=1 pytest tests/functional/test_fast_e2e.py -v
|
|
"""
|
|
|
|
import os
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
try:
|
|
from selenium import webdriver
|
|
from selenium.webdriver.chrome.options import Options
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
|
|
HAS_SELENIUM = True
|
|
except ImportError:
|
|
HAS_SELENIUM = False
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not HAS_SELENIUM or os.environ.get("SELENIUM_UI") != "1",
|
|
reason="Selenium not installed or SELENIUM_UI not set to 1",
|
|
)
|
|
|
|
DASHBOARD_URL = os.environ.get("DASHBOARD_URL", "http://localhost:8000")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def driver():
|
|
"""Single browser instance for all tests (module-scoped for reuse)."""
|
|
opts = Options()
|
|
opts.add_argument("--headless=new") # Headless for speed
|
|
opts.add_argument("--no-sandbox")
|
|
opts.add_argument("--disable-dev-shm-usage")
|
|
opts.add_argument("--disable-gpu")
|
|
opts.add_argument("--window-size=1280,900")
|
|
|
|
d = webdriver.Chrome(options=opts)
|
|
d.implicitly_wait(2) # Reduced from 5s
|
|
yield d
|
|
d.quit()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def dashboard_url():
|
|
"""Verify server is running."""
|
|
try:
|
|
r = httpx.get(f"{DASHBOARD_URL}/health", timeout=3)
|
|
if r.status_code != 200:
|
|
pytest.skip("Dashboard not healthy")
|
|
except Exception:
|
|
pytest.skip(f"Dashboard not reachable at {DASHBOARD_URL}")
|
|
return DASHBOARD_URL
|
|
|
|
|
|
class TestAllPagesLoad:
|
|
"""Single test that checks all pages load - much faster than separate tests."""
|
|
|
|
def test_all_dashboard_pages_exist(self, driver, dashboard_url):
|
|
"""Verify all new feature pages load successfully in one browser session."""
|
|
pages = [
|
|
("/swarm/events", "Event"),
|
|
("/lightning/ledger", "Lightning Ledger"),
|
|
("/memory", "Memory"),
|
|
("/router/status", "Router Status"),
|
|
("/self-modify/queue", "Upgrade"),
|
|
("/swarm/live", "Swarm"), # Live page has "Swarm" not "Live"
|
|
]
|
|
|
|
failures = []
|
|
|
|
for path, expected_text in pages:
|
|
try:
|
|
driver.get(f"{dashboard_url}{path}")
|
|
# Quick check - wait max 5s for any content
|
|
WebDriverWait(driver, 5).until(
|
|
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
|
)
|
|
|
|
# Give a small extra buffer for animations (fadeUp in style.css)
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
# Verify page has expected content
|
|
body_text = driver.find_element(By.TAG_NAME, "body").text
|
|
if expected_text.lower() not in body_text.lower():
|
|
failures.append(f"{path}: missing '{expected_text}'")
|
|
|
|
except Exception as exc:
|
|
failures.append(f"{path}: {type(exc).__name__}")
|
|
|
|
if failures:
|
|
pytest.fail(f"Pages failed to load: {', '.join(failures)}")
|
|
|
|
|
|
class TestAllFeaturesWork:
|
|
"""Combined functional tests - single browser session."""
|
|
|
|
def test_event_log_and_memory_and_ledger_functional(self, driver, dashboard_url):
|
|
"""Test Event Log, Memory, and Ledger functionality in one go."""
|
|
|
|
# 1. Event Log - verify events display
|
|
driver.get(f"{dashboard_url}/swarm/events")
|
|
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
# Should have header and either events or empty state
|
|
body = driver.find_element(By.TAG_NAME, "body").text
|
|
assert "event log" in body.lower(), "Event log page missing header"
|
|
|
|
# Create a task via API to generate an event
|
|
try:
|
|
httpx.post(
|
|
f"{dashboard_url}/swarm/tasks",
|
|
data={"description": "E2E test task"},
|
|
timeout=2,
|
|
)
|
|
except Exception:
|
|
pass # Ignore, just checking page exists
|
|
|
|
# 2. Memory - verify search works
|
|
driver.get(f"{dashboard_url}/memory?query=test")
|
|
WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
|
|
# Should have search input
|
|
search = driver.find_elements(By.CSS_SELECTOR, "input[type='search'], input[name='query']")
|
|
assert search, "Memory page missing search input"
|
|
|
|
# 3. Ledger - verify balance display
|
|
driver.get(f"{dashboard_url}/lightning/ledger")
|
|
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
time.sleep(0.5)
|
|
|
|
body = driver.find_element(By.TAG_NAME, "body").text
|
|
# Should show balance-related text
|
|
has_balance = any(x in body.lower() for x in ["balance", "sats", "transaction", "ledger"])
|
|
assert has_balance, "Ledger page missing balance info"
|
|
|
|
|
|
class TestCascadeRouter:
|
|
"""Cascade Router - combined checks."""
|
|
|
|
def test_router_status_and_navigation(self, driver, dashboard_url):
|
|
"""Verify router status page and nav link in one test."""
|
|
|
|
# Check router status page
|
|
driver.get(f"{dashboard_url}/router/status")
|
|
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
body = driver.find_element(By.TAG_NAME, "body").text
|
|
|
|
# Should show providers or config message
|
|
has_content = any(
|
|
x in body.lower()
|
|
for x in ["provider", "router", "ollama", "config", "status", "registry"]
|
|
)
|
|
assert has_content, "Router status page missing content"
|
|
|
|
# Check nav has router link
|
|
driver.get(dashboard_url)
|
|
WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
|
|
nav_links = driver.find_elements(By.XPATH, "//a[contains(@href, '/router')]")
|
|
assert nav_links, "Navigation missing router link"
|
|
|
|
|
|
class TestUpgradeQueue:
|
|
"""Upgrade Queue - combined checks."""
|
|
|
|
def test_upgrade_queue_page_and_elements(self, driver, dashboard_url):
|
|
"""Verify upgrade queue page loads with expected elements."""
|
|
|
|
driver.get(f"{dashboard_url}/self-modify/queue")
|
|
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
body = driver.find_element(By.TAG_NAME, "body").text
|
|
|
|
# Should have queue header
|
|
assert "upgrade" in body.lower() or "queue" in body.lower(), "Missing queue header"
|
|
|
|
# Should have pending section or empty state
|
|
has_pending = "pending" in body.lower() or "no pending" in body.lower()
|
|
assert has_pending, "Missing pending upgrades section"
|
|
|
|
# Check for approve/reject buttons if upgrades exist
|
|
driver.find_elements(By.XPATH, "//button[contains(text(), 'Approve')]")
|
|
driver.find_elements(By.XPATH, "//button[contains(text(), 'Reject')]")
|
|
|
|
# Either no upgrades (no buttons) or buttons exist
|
|
# This is a soft check - page structure is valid either way
|
|
|
|
|
|
class TestActivityFeed:
|
|
"""Activity Feed - combined checks."""
|
|
|
|
def test_swarm_live_page_and_activity_feed(self, driver, dashboard_url):
|
|
"""Verify swarm live page has activity feed elements."""
|
|
|
|
driver.get(f"{dashboard_url}/swarm/live")
|
|
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.TAG_NAME, "body")))
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
body = driver.find_element(By.TAG_NAME, "body").text
|
|
|
|
# Should have live indicator or activity section
|
|
has_live = any(x in body.lower() for x in ["live", "activity", "swarm", "agents", "tasks"])
|
|
assert has_live, "Swarm live page missing content"
|
|
|
|
# Check for WebSocket connection indicator (if implemented)
|
|
# or just basic structure
|
|
panels = driver.find_elements(By.CSS_SELECTOR, ".card, .panel, .mc-panel")
|
|
assert panels, "Swarm live page missing panels"
|
|
|
|
|
|
class TestFastSmoke:
|
|
"""Ultra-fast smoke tests using HTTP where possible."""
|
|
|
|
def test_all_routes_respond_200(self, dashboard_url):
|
|
"""HTTP-only test - no browser, very fast."""
|
|
routes = [
|
|
"/swarm/events",
|
|
"/lightning/ledger",
|
|
"/memory",
|
|
"/router/status",
|
|
"/self-modify/queue",
|
|
"/swarm/live",
|
|
]
|
|
|
|
failures = []
|
|
|
|
for route in routes:
|
|
try:
|
|
r = httpx.get(f"{dashboard_url}{route}", timeout=3, follow_redirects=True)
|
|
if r.status_code != 200:
|
|
failures.append(f"{route}: {r.status_code}")
|
|
except Exception as exc:
|
|
failures.append(f"{route}: {type(exc).__name__}")
|
|
|
|
if failures:
|
|
pytest.fail(f"Routes failed: {', '.join(failures)}")
|