chore: promote staging to staging-promote/57c397bd-23120362128 (2026-03-16 05:35 UTC) (#1236)

* 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]>
This commit is contained in:
ironclaw-ci[bot]
2026-03-16 08:09:34 +00:00
committed by GitHub
co-authored by Illia Polosukhin Claude Opus 4.6 Nick Pismenkov
parent 57c397bd50
commit fe53f6993f
13 changed files with 1607 additions and 1006 deletions
@@ -0,0 +1,172 @@
"""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