mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 01:19:34 +00:00
* refactor(setup): extract init logic from wizard into owning modules (#1210) * refactor(setup): extract init logic from wizard into owning modules Move database, LLM model discovery, and secrets initialization logic out of the setup wizard and into their owning modules, following the CLAUDE.md principle that module-specific initialization must live in the owning module as a public factory function. Database (src/db/mod.rs, src/config/database.rs): - Add DatabaseConfig::from_postgres_url() and from_libsql_path() - Add connect_without_migrations() for connectivity testing - Add validate_postgres() returning structured PgDiagnostic results LLM (src/llm/models.rs — new file): - Extract 8 model-fetching functions from wizard.rs (~380 lines) - fetch_anthropic_models, fetch_openai_models, fetch_ollama_models, fetch_openai_compatible_models, build_nearai_model_fetch_config, and OpenAI sorting/filtering helpers Secrets (src/secrets/mod.rs): - Add resolve_master_key() unifying env var + keychain resolution - Add crypto_from_hex() convenience wrapper Wizard restructuring (src/setup/wizard.rs): - Replace cfg-gated db_pool/db_backend fields with generic db: Option<Arc<dyn Database>> + db_handles: Option<DatabaseHandles> - Delete 6 backend-specific methods (reconnect_postgres/libsql, test_database_connection_postgres/libsql, run_migrations_postgres/ libsql, create_postgres/libsql_secrets_store) - Simplify persist_settings, try_load_existing_settings, persist_session_to_db, init_secrets_context to backend-agnostic implementations using the new module factories - Eliminate all references to deadpool_postgres, PoolConfig, LibSqlBackend, Store::from_pool, refinery::embed_migrations Net: -878 lines from wizard, +395 lines in owning modules, +378 new. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * test(settings): add wizard re-run regression tests Add 10 tests covering settings preservation during wizard re-runs: - provider_only rerun preserves channels/embeddings/heartbeat - channels_only rerun preserves provider/model/embeddings - quick mode rerun preserves prior channels and heartbeat - full rerun same provider preserves model through merge - full rerun different provider clears model through merge - incremental persist doesn't clobber prior steps - switching DB backend allows fresh connection settings - merge preserves true booleans when overlay has default false - embeddings survive rerun that skips step 5 These cover the scenarios where re-running the wizard would previously risk resetting models, providers, or channel settings. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(setup): eliminate cfg(feature) gates from wizard methods Replace compile-time #[cfg(feature)] dispatch in the wizard with runtime dispatch via DatabaseBackend enum and cfg!() macro constants. - Merge step_database_postgres + step_database_libsql into step_database using runtime backend selection - Rewrite auto_setup_database without feature gates - Remove cfg(feature = "postgres") from mask_password_in_url (pure fn) - Remove cfg(feature = "postgres") from test_mask_password_in_url Only one internal #[cfg(feature = "postgres")] remains: guarding the call to db::validate_postgres() which is itself feature-gated. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(db): fold PG validation into connect_without_migrations Move PostgreSQL prerequisite validation (version >= 15, pgvector) from the wizard into connect_without_migrations() in the db module. The validation now returns DatabaseError directly with user-facing messages, eliminating the PgDiagnostic enum and the last #[cfg(feature)] gate from the wizard. The wizard's test_database_connection() is now a 5-line method that calls the db module factory and stores the result. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review comments [skip-regression-check] - Use .as_ref().map() to avoid partial move of db_config.libsql_path (gemini-code-assist) - Default to available backend when DATABASE_BACKEND is invalid, not unconditionally to Postgres which may not be compiled (Copilot) - Match DatabaseBackend::Postgres explicitly instead of _ => wildcard in connect_with_handles, connect_without_migrations, and create_secrets_store to avoid silently routing LibSql configs through the Postgres path when libsql feature is disabled (Copilot) - Upgrade Ollama connection failure log from info to warn with the base URL for better visibility in wizard UX (Copilot) - Clarify crypto_from_hex doc: SecretsCrypto validates key length, not hex encoding (Copilot) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address zmanian's PR review feedback [skip-regression-check] - Update src/setup/README.md to reflect Arc<dyn Database> flow - Remove stale "Test PostgreSQL connection" doc comment - Replace unwrap_or(0) in validate_postgres with descriptive error - Add NearAiConfig::for_model_discovery() constructor - Narrow pub to pub(crate) for internal model helpers Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address Copilot review comments (quick-mode postgres gate, empty env vars) [skip-regression-check] - Gate DATABASE_URL auto-detection on POSTGRES_AVAILABLE in quick mode so libsql-only builds don't attempt a postgres connection - Match empty-env-var filtering in key source detection to align with resolve_master_key() behavior - Filter empty strings to None in DatabaseConfig::from_libsql_path() for turso_url/turso_token Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> * fix: Telegram bot token validation fails intermittently (HTTP 404) (#1166) * fix: Telegram bot token validation fails intermittently (HTTP 404) * fix: code style * fix * fix * fix * review fix --------- Co-authored-by: Illia Polosukhin <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: Nick Pismenkov <[email protected]>
173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
"""Scenario: Telegram bot token validation - configure modal UI test.
|
|
|
|
Tests the Telegram extension configure modal renders and accepts tokens with colons.
|
|
|
|
Note: The core URL-building logic (colon preservation, no %3A encoding) is verified
|
|
by unit tests in src/extensions/manager.rs. This E2E test verifies the configure modal
|
|
UI can accept Telegram tokens with colons and renders correctly.
|
|
"""
|
|
|
|
import json
|
|
|
|
from helpers import SEL
|
|
|
|
|
|
# ─── Fixture data ─────────────────────────────────────────────────────────────
|
|
|
|
_TELEGRAM_EXTENSION = {
|
|
"name": "telegram",
|
|
"display_name": "Telegram",
|
|
"kind": "wasm_channel",
|
|
"description": "Telegram bot channel",
|
|
"url": None,
|
|
"active": False,
|
|
"authenticated": False,
|
|
"has_auth": True,
|
|
"needs_setup": True,
|
|
"tools": [],
|
|
"activation_status": "installed",
|
|
"activation_error": None,
|
|
}
|
|
|
|
_TELEGRAM_SECRETS = [
|
|
{
|
|
"name": "telegram_bot_token",
|
|
"prompt": "Telegram Bot Token",
|
|
"provided": False,
|
|
"optional": False,
|
|
"auto_generate": False,
|
|
}
|
|
]
|
|
|
|
|
|
# ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
async def test_telegram_configure_modal_renders(page):
|
|
"""
|
|
Telegram extension configure modal renders with correct fields.
|
|
|
|
Verifies that the configure modal appears with the Telegram bot token field
|
|
and all expected UI elements are present.
|
|
"""
|
|
ext_body = json.dumps({"extensions": [_TELEGRAM_EXTENSION]})
|
|
|
|
async def handle_ext_list(route):
|
|
if route.request.url.endswith("/api/extensions"):
|
|
await route.fulfill(
|
|
status=200, content_type="application/json", body=ext_body
|
|
)
|
|
else:
|
|
await route.continue_()
|
|
|
|
await page.route("**/api/extensions*", handle_ext_list)
|
|
|
|
async def handle_setup(route):
|
|
if route.request.method == "GET":
|
|
await route.fulfill(
|
|
status=200,
|
|
content_type="application/json",
|
|
body=json.dumps({"secrets": _TELEGRAM_SECRETS}),
|
|
)
|
|
else:
|
|
await route.continue_()
|
|
|
|
await page.route("**/api/extensions/telegram/setup", handle_setup)
|
|
await page.evaluate("showConfigureModal('telegram')")
|
|
modal = page.locator(SEL["configure_modal"])
|
|
await modal.wait_for(state="visible", timeout=5000)
|
|
|
|
# Modal should contain the extension name and token prompt
|
|
modal_text = await modal.text_content()
|
|
assert "telegram" in modal_text.lower()
|
|
assert "bot token" in modal_text.lower()
|
|
|
|
# Input field should be present
|
|
input_field = page.locator(SEL["configure_input"])
|
|
assert await input_field.is_visible()
|
|
|
|
|
|
async def test_telegram_token_input_accepts_colon_format(page):
|
|
"""
|
|
Telegram bot token input accepts tokens with colon separator.
|
|
|
|
Verifies that a token in the format `numeric_id:alphanumeric_string`
|
|
can be entered without browser-side validation errors.
|
|
"""
|
|
ext_body = json.dumps({"extensions": [_TELEGRAM_EXTENSION]})
|
|
|
|
async def handle_ext_list(route):
|
|
if route.request.url.endswith("/api/extensions"):
|
|
await route.fulfill(
|
|
status=200, content_type="application/json", body=ext_body
|
|
)
|
|
else:
|
|
await route.continue_()
|
|
|
|
await page.route("**/api/extensions*", handle_ext_list)
|
|
|
|
async def handle_setup(route):
|
|
if route.request.method == "GET":
|
|
await route.fulfill(
|
|
status=200,
|
|
content_type="application/json",
|
|
body=json.dumps({"secrets": _TELEGRAM_SECRETS}),
|
|
)
|
|
|
|
await page.route("**/api/extensions/telegram/setup", handle_setup)
|
|
await page.evaluate("showConfigureModal('telegram')")
|
|
await page.locator(SEL["configure_modal"]).wait_for(state="visible", timeout=5000)
|
|
|
|
# Enter a valid Telegram bot token with colon
|
|
token_value = "123456789:AABBccDDeeFFgg_Test-Token"
|
|
input_field = page.locator(SEL["configure_input"])
|
|
await input_field.fill(token_value)
|
|
|
|
# Verify the value was entered and colon is preserved
|
|
entered_value = await input_field.input_value()
|
|
assert entered_value == token_value
|
|
assert ":" in entered_value, "Colon should be preserved in token"
|
|
assert "%3A" not in entered_value, "Colon should not be URL-encoded in input"
|
|
|
|
|
|
async def test_telegram_token_with_underscores_and_hyphens(page):
|
|
"""
|
|
Telegram tokens with hyphens and underscores are accepted.
|
|
|
|
Verifies that valid Telegram token characters (hyphens, underscores) are
|
|
properly accepted by the input field.
|
|
"""
|
|
ext_body = json.dumps({"extensions": [_TELEGRAM_EXTENSION]})
|
|
|
|
async def handle_ext_list(route):
|
|
if route.request.url.endswith("/api/extensions"):
|
|
await route.fulfill(
|
|
status=200, content_type="application/json", body=ext_body
|
|
)
|
|
else:
|
|
await route.continue_()
|
|
|
|
await page.route("**/api/extensions*", handle_ext_list)
|
|
|
|
async def handle_setup(route):
|
|
if route.request.method == "GET":
|
|
await route.fulfill(
|
|
status=200,
|
|
content_type="application/json",
|
|
body=json.dumps({"secrets": _TELEGRAM_SECRETS}),
|
|
)
|
|
|
|
await page.route("**/api/extensions/telegram/setup", handle_setup)
|
|
await page.evaluate("showConfigureModal('telegram')")
|
|
await page.locator(SEL["configure_modal"]).wait_for(state="visible", timeout=5000)
|
|
|
|
# Token with hyphens and underscores
|
|
token_value = "987654321:ABCD-EFgh_ijkl-MNOP_qrst"
|
|
input_field = page.locator(SEL["configure_input"])
|
|
await input_field.fill(token_value)
|
|
|
|
# Verify the value was entered correctly with all characters preserved
|
|
entered_value = await input_field.input_value()
|
|
assert entered_value == token_value
|
|
assert "-" in entered_value
|
|
assert "_" in entered_value
|