mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat(gateway): full settings page polish with all tiers - Backend: add ActiveConfigSnapshot to expose resolved LLM backend, model, and enabled channels via /api/gateway/status - Add missing Agent settings (daily cost cap, actions/hour, local tools) - Add Sandbox, Routines, Safety, Skills, and Search setting groups - Settings import/export (JSON download + file upload) - Active env defaults shown as placeholders in Inference settings - Styled confirmation modals replace window.confirm() for remove actions - Global restart banner persists across settings subtab switches - Client-side validation with min/max constraints on number inputs - Accessibility: aria-label on inputs, role=status on save indicators - Settings search filters rows across current subtab - Smooth CSS transitions for conditional field visibility (showWhen) - Tunnel settings in Channels subtab - Mobile responsive settings layout at 768px breakpoint - i18n keys for toolbar, search, and import/export in en + zh-CN Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat(gateway): polish settings page and remove registered tools debug section Remove the "Registered Tools" table from the extensions tab (debug info not useful to end users), clean up associated CSS/i18n/JS. Additional settings page UI polish: extension card state styling, layout refinements. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): address PR review feedback [skip-regression-check] - Use refreshCurrentSettingsTab() in SSE event handlers to reduce duplication - Remove unused formatGroupName/formatSettingLabel helpers - Use i18n keys for MCP Configure/Reconfigure buttons - Add data-i18n-placeholder to settings search input - Remove data-i18n from confirm modal button (set dynamically by showConfirmModal) - Fix cargo fmt in main.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(e2e): update tests for unified settings tab layout [skip-regression-check] - Update TABS list: replace extensions/skills with settings - Add settings_subtab/settings_subpanel selectors to helpers - Update test_connection, test_skills, test_extensions, test_wasm_lifecycle to navigate via Settings > subtab instead of top-level tabs - Move MCP card tests to use go_to_mcp() helper (MCP is now a separate subtab) - Remove tools table tests and mock_ext_apis tools= parameter - Fix CSP violation: replace inline onclick on confirm modal cancel button Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): address second round of PR review feedback [skip-regression-check] - Use I18n.t() for MCP empty state, export/import toasts, confirm modal - Fix CLI channel card using wrong channel key ('repl' -> 'cli') - Fix settings search counting hidden rows as visible - Add aria-label i18n for settings search input - Add common.loadFailed i18n key (en + zh-CN) - Update E2E tests: WASM channel tests use Channels subtab, remove tests use custom confirm modal instead of window.confirm Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(e2e): fix WASM channel card selector and skills remove confirm [skip-regression-check] - WASM channel tests: filter by display name to avoid matching built-in channel cards in the Channels subtab - Skills remove test: click confirm modal button instead of using window.confirm (skill removal now uses custom confirm modal) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): address third round of PR review feedback [skip-regression-check] - approval_needed SSE: refresh any active settings subtab, not just Extensions — approvals can surface from Channels/MCP setup flows too - renderCardsSkeleton: remove nested .extensions-list wrapper that caused skeleton cards to render constrained inside grid cells Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(e2e): fix auth_completed reload test race condition [skip-regression-check] Use expect_response to deterministically wait for the /api/extensions reload triggered by handleAuthCompleted → refreshCurrentSettingsTab, instead of a fixed 600ms sleep that was too short under CI load. Also remove stale /api/extensions/tools route handler. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(e2e): debug auth_completed reload test with function counter [skip-regression-check] Inject a counter wrapper around refreshCurrentSettingsTab to verify it's actually called, and wait for the async fetch to complete before asserting the reload count. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(gateway): localize all settings labels, descriptions, and channel cards [skip-regression-check] Move 120+ hardcoded strings in settings definitions (INFERENCE_SETTINGS, AGENT_SETTINGS, NETWORKING_SETTINGS) and channel card labels to i18n keys. Render functions now resolve labels via I18n.t() so the settings page translates when switching locales. Covers: group titles, setting labels/descriptions, built-in channel names/descriptions, and the "No settings found" empty state. Both en.js and zh-CN.js updated with all new cfg.* and channels.* keys. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): localize remaining hardcoded UI strings [skip-regression-check] - Fix export error toast using wrong i18n key (importFailed → exportFailed) - Replace "Failed to load settings:" with I18n.t('common.loadFailed') - Localize renderBuiltinChannelCard: "Built-in", "Active", "Inactive" - Localize settings placeholders: "env: ", "env default", "use env default" - Localize "✓ Saved" indicator - Add new i18n keys to both en.js and zh-CN.js Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): confirm modal a11y, Esc/click-outside, search guard [skip-regression-check] - Add role="dialog", aria-modal="true", aria-labelledby to confirm modal - Focus confirm button when modal opens - Close modal on Escape key or overlay click - Skip settings search on non-settings panels (Extensions/MCP/Skills/Channels) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): boolean tri-state, search reset on subtab switch, stale model suggestions [skip-regression-check] Address PR review feedback: - Boolean settings now use a tri-state select (env default / On / Off) instead of a checkbox, matching the pattern used by other select settings and allowing users to revert to the env default - Clear search input when switching settings subtabs so stale filters don't carry over to the new panel - Always assign model suggestions (even empty array) so stale IDs from a previous successful /v1/models fetch don't persist when the endpoint later returns empty Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): auth_completed handler, bedrock_cross_region select, integer-only number inputs [skip-regression-check] Address PR review feedback: - auth_completed SSE listener now delegates to handleAuthCompleted(data) instead of inlining logic with a bare closeConfigureModal() call, so only the matching extension's modal is dismissed - bedrock_cross_region changed from free text to select with the four valid values (us/eu/apac/global), matching backend validation - Number settings now use step=1 and parseInt() instead of parseFloat(), preventing fractional values that the backend (u32/u64) would reject Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
196 lines
7.7 KiB
Python
196 lines
7.7 KiB
Python
"""Shared helpers for E2E tests."""
|
||
|
||
import asyncio
|
||
import hashlib
|
||
import hmac
|
||
import re
|
||
import time
|
||
|
||
import httpx
|
||
|
||
# -- DOM Selectors --------------------------------------------------------
|
||
# Keep all selectors in one place so changes to the frontend only need
|
||
# one update.
|
||
|
||
SEL = {
|
||
# Auth
|
||
"auth_screen": "#auth-screen",
|
||
"token_input": "#token-input",
|
||
# Connection
|
||
"sse_status": "#sse-status",
|
||
# Tabs
|
||
"tab_button": '.tab-bar button[data-tab="{tab}"]',
|
||
"tab_panel": "#tab-{tab}",
|
||
# Chat
|
||
"chat_input": "#chat-input",
|
||
"chat_messages": "#chat-messages",
|
||
"message_user": "#chat-messages .message.user",
|
||
"message_assistant": "#chat-messages .message.assistant",
|
||
# Skills
|
||
"skill_search_input": "#skill-search-input",
|
||
"skill_search_results": "#skill-search-results",
|
||
"skill_search_result": ".skill-search-result",
|
||
"skill_installed": "#skills-list .ext-card",
|
||
# SSE status
|
||
"sse_dot": "#sse-dot",
|
||
# Approval overlay
|
||
"approval_card": ".approval-card",
|
||
"approval_header": ".approval-header",
|
||
"approval_tool_name": ".approval-tool-name",
|
||
"approval_description": ".approval-description",
|
||
"approval_params_toggle": ".approval-params-toggle",
|
||
"approval_params": ".approval-params",
|
||
"approval_actions": ".approval-actions",
|
||
"approval_approve_btn": ".approval-actions button.approve",
|
||
"approval_always_btn": ".approval-actions button.always",
|
||
"approval_deny_btn": ".approval-actions button.deny",
|
||
"approval_resolved": ".approval-resolved",
|
||
# Settings subtabs
|
||
"settings_subtab": '.settings-subtab[data-settings-subtab="{subtab}"]',
|
||
"settings_subpanel": "#settings-{subtab}",
|
||
# Extensions section
|
||
"extensions_list": "#extensions-list",
|
||
"available_wasm_list": "#available-wasm-list",
|
||
"mcp_servers_list": "#mcp-servers-list",
|
||
# Extensions tab – cards
|
||
"ext_card_installed": "#extensions-list .ext-card",
|
||
"ext_card_available": "#available-wasm-list .ext-card.ext-available",
|
||
"ext_card_mcp": "#mcp-servers-list .ext-card",
|
||
"ext_name": ".ext-name",
|
||
"ext_kind": ".ext-kind",
|
||
"ext_auth_dot": ".ext-auth-dot",
|
||
"ext_auth_dot_authed": ".ext-auth-dot.authed",
|
||
"ext_auth_dot_unauthed": ".ext-auth-dot.unauthed",
|
||
"ext_active_label": ".ext-active-label",
|
||
"ext_pairing_label": ".ext-pairing-label",
|
||
"ext_error": ".ext-error",
|
||
"ext_tools": ".ext-tools",
|
||
# Extensions tab – action buttons
|
||
"ext_install_btn": ".btn-ext.install",
|
||
"ext_remove_btn": ".btn-ext.remove",
|
||
"ext_activate_btn": ".btn-ext.activate",
|
||
"ext_configure_btn": ".btn-ext.configure",
|
||
# Configure modal
|
||
"configure_overlay": ".configure-overlay",
|
||
"configure_modal": ".configure-modal",
|
||
"configure_field": ".configure-field",
|
||
"configure_input": ".configure-modal input[type='password']",
|
||
"configure_save_btn": ".configure-actions button.btn-ext.activate",
|
||
"configure_cancel_btn": ".configure-actions button.btn-ext.remove",
|
||
"field_provided": ".field-provided",
|
||
"field_autogen": ".field-autogen",
|
||
"field_optional": ".field-optional",
|
||
# Auth card (SSE-triggered, injected into chat-messages)
|
||
"auth_card": ".auth-card",
|
||
"auth_header": ".auth-header",
|
||
"auth_instructions": ".auth-instructions",
|
||
"auth_oauth_btn": ".auth-oauth",
|
||
"auth_token_input": ".auth-token-input input",
|
||
"auth_submit_btn": ".auth-submit",
|
||
"auth_cancel_btn": ".auth-cancel",
|
||
"auth_error": ".auth-error",
|
||
# WASM channel progress stepper
|
||
"ext_stepper": ".ext-stepper",
|
||
"stepper_step": ".stepper-step",
|
||
"stepper_circle": ".stepper-circle",
|
||
# Confirm modal (custom, replaces window.confirm)
|
||
"confirm_modal": "#confirm-modal",
|
||
"confirm_modal_btn": "#confirm-modal-btn",
|
||
"confirm_modal_cancel": "#confirm-modal-cancel-btn",
|
||
# Channels subtab – cards
|
||
"channels_ext_card": "#settings-channels-content .ext-card",
|
||
# Toast notifications
|
||
"toast": ".toast",
|
||
"toast_success": ".toast.toast-success",
|
||
"toast_error": ".toast.toast-error",
|
||
"toast_info": ".toast.toast-info",
|
||
# Jobs / routines
|
||
"jobs_tbody": "#jobs-tbody",
|
||
"job_row": "#jobs-tbody .job-row",
|
||
"jobs_empty": "#jobs-empty",
|
||
"routines_tbody": "#routines-tbody",
|
||
"routine_row": "#routines-tbody .routine-row",
|
||
"routines_empty": "#routines-empty",
|
||
}
|
||
|
||
TABS = ["chat", "memory", "jobs", "routines", "settings"]
|
||
|
||
# Auth token used across all tests
|
||
AUTH_TOKEN = "e2e-test-token"
|
||
OWNER_SCOPE_ID = "e2e-owner-scope"
|
||
HTTP_WEBHOOK_SECRET = "e2e-http-webhook-secret"
|
||
|
||
|
||
async def wait_for_ready(url: str, *, timeout: float = 60, interval: float = 0.5):
|
||
"""Poll a URL until it returns 200 or timeout."""
|
||
deadline = time.monotonic() + timeout
|
||
async with httpx.AsyncClient() as client:
|
||
while time.monotonic() < deadline:
|
||
try:
|
||
resp = await client.get(url, timeout=5)
|
||
if resp.status_code == 200:
|
||
return
|
||
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException):
|
||
pass
|
||
await asyncio.sleep(interval)
|
||
raise TimeoutError(f"Service at {url} not ready after {timeout}s")
|
||
|
||
|
||
async def wait_for_port_line(process, pattern: str, *, timeout: float = 60) -> int:
|
||
"""Read process stdout line by line until a port-bearing line matches."""
|
||
deadline = time.monotonic() + timeout
|
||
while time.monotonic() < deadline:
|
||
remaining = deadline - time.monotonic()
|
||
if remaining <= 0:
|
||
break
|
||
try:
|
||
line = await asyncio.wait_for(process.stdout.readline(), timeout=remaining)
|
||
except asyncio.TimeoutError:
|
||
break
|
||
decoded = line.decode("utf-8", errors="replace").strip()
|
||
if match := re.search(pattern, decoded):
|
||
return int(match.group(1))
|
||
raise TimeoutError(f"Port pattern '{pattern}' not found in stdout after {timeout}s")
|
||
|
||
|
||
# -- API helpers -----------------------------------------------------------
|
||
|
||
def auth_headers() -> dict[str, str]:
|
||
"""Return Authorization header dict for authenticated API calls."""
|
||
return {"Authorization": f"Bearer {AUTH_TOKEN}"}
|
||
|
||
|
||
async def api_get(base_url: str, path: str, **kwargs) -> httpx.Response:
|
||
"""Make an authenticated GET request to the ironclaw API."""
|
||
async with httpx.AsyncClient() as client:
|
||
return await client.get(
|
||
f"{base_url}{path}",
|
||
headers=auth_headers(),
|
||
timeout=kwargs.pop("timeout", 10),
|
||
**kwargs,
|
||
)
|
||
|
||
|
||
async def api_post(base_url: str, path: str, **kwargs) -> httpx.Response:
|
||
"""Make an authenticated POST request to the ironclaw API."""
|
||
async with httpx.AsyncClient() as client:
|
||
return await client.post(
|
||
f"{base_url}{path}",
|
||
headers=auth_headers(),
|
||
timeout=kwargs.pop("timeout", 10),
|
||
**kwargs,
|
||
)
|
||
|
||
|
||
def signed_http_webhook_headers(body: bytes) -> dict[str, str]:
|
||
"""Return headers for the owner-scoped HTTP webhook channel."""
|
||
digest = hmac.new(
|
||
HTTP_WEBHOOK_SECRET.encode("utf-8"),
|
||
body,
|
||
hashlib.sha256,
|
||
).hexdigest()
|
||
return {
|
||
"Content-Type": "application/json",
|
||
"X-Hub-Signature-256": f"sha256={digest}",
|
||
}
|