forked from Rockachopa/Timmy-time-dashboard
This commit implements six major features: 1. Event Log System (src/swarm/event_log.py) - SQLite-based audit trail for all swarm events - Task lifecycle tracking (created, assigned, completed, failed) - Agent lifecycle tracking (joined, left, status changes) - Integrated with coordinator for automatic logging - Dashboard page at /swarm/events 2. Lightning Ledger (src/lightning/ledger.py) - Transaction tracking for Lightning Network payments - Balance calculations (incoming, outgoing, net, available) - Integrated with payment_handler for automatic logging - Dashboard page at /lightning/ledger 3. Semantic Memory / Vector Store (src/memory/vector_store.py) - Embedding-based similarity search for Echo agent - Fallback to keyword matching if sentence-transformers unavailable - Personal facts storage and retrieval - Dashboard page at /memory 4. Cascade Router Integration (src/timmy/cascade_adapter.py) - Automatic LLM failover between providers (Ollama → AirLLM → API) - Circuit breaker pattern for failing providers - Metrics tracking per provider (latency, error rates) - Dashboard status page at /router/status 5. Self-Upgrade Approval Queue (src/upgrades/) - State machine for self-modifications: proposed → approved/rejected → applied/failed - Human approval required before applying changes - Git integration for branch management - Dashboard queue at /self-modify/queue 6. Real-Time Activity Feed (src/events/broadcaster.py) - WebSocket-based live activity streaming - Bridges event_log to dashboard clients - Activity panel on /swarm/live Tests: - 101 unit tests passing - 4 new E2E test files for Selenium testing - Run with: SELENIUM_UI=1 pytest tests/functional/ -v --headed Documentation: - 6 ADRs (017-022) documenting architecture decisions - Implementation summary in docs/IMPLEMENTATION_SUMMARY.md - Architecture diagram in docs/architecture-v2.md
290 lines
13 KiB
Python
290 lines
13 KiB
Python
"""E2E tests for new features: Event Log, Ledger, Memory.
|
|
|
|
REQUIRES: Dashboard running at http://localhost:8000
|
|
RUN: SELENIUM_UI=1 pytest tests/functional/test_new_features_e2e.py -v
|
|
|
|
These tests verify the new features through the actual UI:
|
|
1. Event Log - viewable in dashboard
|
|
2. Lightning Ledger - balance and transactions visible
|
|
3. Semantic Memory - searchable memory browser
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
from selenium import webdriver
|
|
from selenium.webdriver.chrome.options import Options
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.common.keys import Keys
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.environ.get("SELENIUM_UI") != "1",
|
|
reason="Set SELENIUM_UI=1 to run Selenium UI tests",
|
|
)
|
|
|
|
@pytest.fixture(scope="module")
|
|
def driver():
|
|
"""Headless Chrome WebDriver."""
|
|
opts = Options()
|
|
opts.add_argument("--headless=new")
|
|
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(5)
|
|
yield d
|
|
d.quit()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def dashboard_url(live_server):
|
|
"""Base URL for dashboard (from live_server fixture)."""
|
|
return live_server
|
|
|
|
|
|
def _wait_for_element(driver, selector, timeout=10):
|
|
"""Wait for element to appear."""
|
|
return WebDriverWait(driver, timeout).until(
|
|
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# EVENT LOG E2E TESTS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class TestEventLogUI:
|
|
"""Event Log feature - viewable through dashboard."""
|
|
|
|
def test_event_log_page_exists(self, driver):
|
|
"""Event log page loads at /swarm/events."""
|
|
driver.get(f"{dashboard_url}/swarm/events")
|
|
header = _wait_for_element(driver, "h1, h2, .page-title", timeout=10)
|
|
assert "event" in header.text.lower() or "log" in header.text.lower()
|
|
|
|
def test_event_log_shows_recent_events(self, driver):
|
|
"""Event log displays events table with timestamp, type, source."""
|
|
driver.get(f"{dashboard_url}/swarm/events")
|
|
|
|
# Should show events table or "no events" message
|
|
table = driver.find_elements(By.CSS_SELECTOR, ".events-table, table")
|
|
no_events = driver.find_elements(By.XPATH, "//*[contains(text(), 'no events') or contains(text(), 'No events')]")
|
|
|
|
assert table or no_events, "Should show events table or 'no events' message"
|
|
|
|
def test_event_log_filters_by_type(self, driver):
|
|
"""Can filter events by type (task, agent, system)."""
|
|
driver.get(f"{dashboard_url}/swarm/events")
|
|
|
|
# Look for filter dropdown or buttons
|
|
filters = driver.find_elements(By.CSS_SELECTOR, "select[name='type'], .filter-btn, [data-filter]")
|
|
|
|
# If filters exist, test them
|
|
if filters:
|
|
# Select 'task' filter
|
|
filter_select = driver.find_element(By.CSS_SELECTOR, "select[name='type']")
|
|
filter_select.click()
|
|
driver.find_element(By.CSS_SELECTOR, "option[value='task']").click()
|
|
|
|
# Wait for filtered results
|
|
time.sleep(1)
|
|
|
|
# Check URL changed or content updated
|
|
events = driver.find_elements(By.CSS_SELECTOR, ".event-row, tr")
|
|
# Just verify no error occurred
|
|
|
|
def test_event_log_shows_task_events_after_task_created(self, driver):
|
|
"""Creating a task generates visible event log entries."""
|
|
# First create a task via API
|
|
import httpx
|
|
task_desc = f"E2E test task {time.time()}"
|
|
httpx.post(f"{dashboard_url}/swarm/tasks", data={"description": task_desc})
|
|
|
|
time.sleep(1) # Wait for event to be logged
|
|
|
|
# Now check event log
|
|
driver.get(f"{dashboard_url}/swarm/events")
|
|
|
|
# Should see the task creation event
|
|
page_text = driver.find_element(By.TAG_NAME, "body").text
|
|
assert "task.created" in page_text.lower() or "task created" in page_text.lower()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# LIGHTNING LEDGER E2E TESTS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class TestLedgerUI:
|
|
"""Lightning Ledger - balance and transactions visible in dashboard."""
|
|
|
|
def test_ledger_page_exists(self, driver):
|
|
"""Ledger page loads at /lightning/ledger."""
|
|
driver.get(f"{dashboard_url}/lightning/ledger")
|
|
header = _wait_for_element(driver, "h1, h2, .page-title", timeout=10)
|
|
assert "ledger" in header.text.lower() or "transaction" in header.text.lower()
|
|
|
|
def test_ledger_shows_balance(self, driver):
|
|
"""Ledger displays current balance."""
|
|
driver.get(f"{dashboard_url}/lightning/ledger")
|
|
|
|
# Look for balance display
|
|
balance = driver.find_elements(By.CSS_SELECTOR, ".balance, .sats-balance, [class*='balance']")
|
|
balance_text = driver.find_elements(By.XPATH, "//*[contains(text(), 'sats') or contains(text(), 'SATS')]")
|
|
|
|
assert balance or balance_text, "Should show balance in sats"
|
|
|
|
def test_ledger_shows_transactions(self, driver):
|
|
"""Ledger displays transaction history."""
|
|
driver.get(f"{dashboard_url}/lightning/ledger")
|
|
|
|
# Should show transactions table or "no transactions" message
|
|
table = driver.find_elements(By.CSS_SELECTOR, ".transactions-table, table")
|
|
empty = driver.find_elements(By.XPATH, "//*[contains(text(), 'no transaction') or contains(text(), 'No transaction')]")
|
|
|
|
assert table or empty, "Should show transactions or empty state"
|
|
|
|
def test_ledger_transaction_has_required_fields(self, driver):
|
|
"""Each transaction shows: hash, amount, status, timestamp."""
|
|
driver.get(f"{dashboard_url}/lightning/ledger")
|
|
|
|
rows = driver.find_elements(By.CSS_SELECTOR, ".transaction-row, tbody tr")
|
|
|
|
if rows:
|
|
# Check first row has expected fields
|
|
first_row = rows[0]
|
|
text = first_row.text.lower()
|
|
|
|
# Should have some of these indicators
|
|
has_amount = any(x in text for x in ["sats", "sat", "000"])
|
|
has_status = any(x in text for x in ["pending", "settled", "failed"])
|
|
|
|
assert has_amount, "Transaction should show amount"
|
|
assert has_status, "Transaction should show status"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# SEMANTIC MEMORY E2E TESTS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class TestMemoryUI:
|
|
"""Semantic Memory - searchable memory browser."""
|
|
|
|
def test_memory_page_exists(self, driver):
|
|
"""Memory browser loads at /memory."""
|
|
driver.get(f"{dashboard_url}/memory")
|
|
header = _wait_for_element(driver, "h1, h2, .page-title", timeout=10)
|
|
assert "memory" in header.text.lower()
|
|
|
|
def test_memory_has_search_box(self, driver):
|
|
"""Memory page has search input."""
|
|
driver.get(f"{dashboard_url}/memory")
|
|
|
|
search = driver.find_elements(By.CSS_SELECTOR, "input[type='search'], input[name='query'], .search-input")
|
|
assert search, "Should have search input"
|
|
|
|
def test_memory_search_returns_results(self, driver):
|
|
"""Search returns memory entries with relevance scores."""
|
|
driver.get(f"{dashboard_url}/memory")
|
|
|
|
search_input = driver.find_element(By.CSS_SELECTOR, "input[type='search'], input[name='query']")
|
|
search_input.send_keys("test query")
|
|
search_input.send_keys(Keys.RETURN)
|
|
|
|
time.sleep(2) # Wait for search results
|
|
|
|
# Should show results or "no results"
|
|
results = driver.find_elements(By.CSS_SELECTOR, ".memory-entry, .search-result")
|
|
no_results = driver.find_elements(By.XPATH, "//*[contains(text(), 'no results') or contains(text(), 'No results')]")
|
|
|
|
assert results or no_results, "Should show search results or 'no results'"
|
|
|
|
def test_memory_shows_entry_content(self, driver):
|
|
"""Memory entries show content, source, and timestamp."""
|
|
driver.get(f"{dashboard_url}/memory")
|
|
|
|
entries = driver.find_elements(By.CSS_SELECTOR, ".memory-entry")
|
|
|
|
if entries:
|
|
first = entries[0]
|
|
text = first.text
|
|
|
|
# Should have content and source
|
|
has_source = any(x in text.lower() for x in ["source:", "from", "by"])
|
|
has_time = any(x in text.lower() for x in ["202", ":", "ago"])
|
|
|
|
assert len(text) > 10, "Entry should have content"
|
|
|
|
def test_memory_add_fact_button(self, driver):
|
|
"""Can add personal fact through UI."""
|
|
driver.get(f"{dashboard_url}/memory")
|
|
|
|
# Look for add fact button or form
|
|
add_btn = driver.find_elements(By.XPATH, "//button[contains(text(), 'Add') or contains(text(), 'New')]")
|
|
add_form = driver.find_elements(By.CSS_SELECTOR, "form[action*='memory'], .add-memory-form")
|
|
|
|
assert add_btn or add_form, "Should have way to add new memory"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# INTEGRATION E2E TESTS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
class TestFeatureIntegration:
|
|
"""Integration tests - features work together."""
|
|
|
|
def test_creating_task_creates_event_and_appears_in_log(self, driver):
|
|
"""Full flow: Create task → event logged → visible in event log UI."""
|
|
import httpx
|
|
|
|
# Create task via API
|
|
task_desc = f"Integration test {time.time()}"
|
|
response = httpx.post(
|
|
f"{dashboard_url}/swarm/tasks",
|
|
data={"description": task_desc}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
time.sleep(1) # Wait for event log
|
|
|
|
# Check event log UI
|
|
driver.get(f"{dashboard_url}/swarm/events")
|
|
page_text = driver.find_element(By.TAG_NAME, "body").text
|
|
|
|
# Should see task creation
|
|
assert "task" in page_text.lower()
|
|
|
|
def test_swarm_live_page_shows_agent_events(self, driver):
|
|
"""Swarm live page shows real-time agent activity."""
|
|
driver.get(f"{dashboard_url}/swarm/live")
|
|
|
|
# Should show activity feed or status
|
|
feed = driver.find_elements(By.CSS_SELECTOR, ".activity-feed, .events-list, .live-feed")
|
|
agents = driver.find_elements(By.CSS_SELECTOR, ".agent-status, .swarm-status")
|
|
|
|
assert feed or agents, "Should show activity feed or agent status"
|
|
|
|
def test_navigation_between_new_features(self, driver):
|
|
"""Can navigate between Event Log, Ledger, and Memory pages."""
|
|
# Start at home
|
|
driver.get(dashboard_url)
|
|
|
|
# Find and click link to events
|
|
event_links = driver.find_elements(By.XPATH, "//a[contains(@href, '/swarm/events') or contains(text(), 'Events')]")
|
|
if event_links:
|
|
event_links[0].click()
|
|
time.sleep(1)
|
|
assert "/swarm/events" in driver.current_url
|
|
|
|
# Navigate to ledger
|
|
driver.get(f"{dashboard_url}/lightning/ledger")
|
|
assert "/lightning/ledger" in driver.current_url
|
|
|
|
# Navigate to memory
|
|
driver.get(f"{dashboard_url}/memory")
|
|
assert "/memory" in driver.current_url
|