Files
optimclaw/tests/module_init_integration.rs
94d101924e refactor: encapsulate leaked abstractions into owning modules (#778)
* refactor: encapsulate leaked abstractions from main.rs and app.rs into owning modules

Move module-specific initialization logic out of main.rs (1222→665 lines, -46%) and
app.rs (944→780 lines, -17%) into their respective owning modules as public factory
functions. This enforces separation of concerns so that adding a new DB backend, MCP
transport, or channel doesn't require editing main.rs/app.rs.

Key changes:
- Tracing init functions → src/tracing_fmt.rs
- DB connection factory (connect_with_handles + DatabaseHandles) → src/db/mod.rs
- Secrets store factory (create_secrets_store) → src/secrets/mod.rs
- MCP transport dispatch factory (create_client_from_config) → src/tools/mcp/factory.rs
- Orchestrator setup (setup_orchestrator + OrchestratorSetup) → src/orchestrator/mod.rs
- WASM channel setup (setup_wasm_channels) → src/channels/wasm/setup.rs
- Worker entry points (run_worker, run_claude_bridge) → src/worker/mod.rs
- Shared CLI secrets init (init_secrets_store) → src/cli/mod.rs
- Tunnel startup (start_managed_tunnel) → src/tunnel/mod.rs
- Onboard check (check_onboard_needed) → src/setup/mod.rs
- ExtensionManager unified MCP: uses create_client_from_config via McpProcessManager,
  enabling stdio/Unix transports for hot-activated MCP servers
- Deduplicated ~130 lines of secrets store init across cli/mcp.rs and cli/tool.rs
- CLAUDE.md updated with module-owned initialization guideline

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: address review feedback — deduplicate db factory, extract channel helper

- connect_from_config() now delegates to connect_with_handles() to eliminate
  duplicated backend-matching logic (Copilot review feedback)
- Extract register_channel() helper from setup_wasm_channels() loop body
  to improve readability (Gemini review feedback)

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix rustfmt line wrapping in setup_wasm_channels

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add integration test for module-owned initialization factories

Exercises the full factory chain end-to-end to verify nothing was lost
when initialization logic was moved from main.rs/app.rs into owning modules:

- connect_with_handles returns Database + populated backend handles
- connect_from_config delegates correctly (produces working Database)
- secrets::create_secrets_store builds working store from DatabaseHandles
- db::create_secrets_store standalone factory round-trips secrets
- Both secrets factories produce compatible stores (cross-read works)
- ExtensionManager constructs with McpProcessManager and is functional
- DatabaseHandles default is empty

All tests run without external services using libsql in-memory/tempfile.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: wire cli/mcp.rs and cli/tool.rs to shared init_secrets_store()

Both files had inline implementations identical to cli::init_secrets_store().
Replace with delegation to complete the claimed deduplication.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix rustfmt line wrapping in integration test

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(review): remove unused Config import and deduplicate Error Handling section

- Remove `#[allow(unused_imports)]` and unused `use crate::config::Config`
  from cli/tool.rs (no longer needed after delegating to shared
  `cli::init_secrets_store()`)
- Remove duplicate Error Handling subsection from CLAUDE.md Key Patterns
  (all four bullets already exist in Code Style section and
  review-discipline.md)

Addresses Copilot review comments.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(review): address remaining Copilot review comments

- secrets/mod.rs: clarify docstring that None is a normal no-db condition
- app.rs: add comment explaining the empty_handles fallback path
- orchestrator/mod.rs: combine duplicated sandbox condition into single block
- setup/mod.rs: document env var reads and thread-safety caveat

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Henry Park <[email protected]>
2026-03-10 04:39:51 +00:00

238 lines
8.2 KiB
Rust

//! Integration test for module-owned initialization factories.
//!
//! Verifies that the refactored factory functions in `db`, `secrets`,
//! `orchestrator`, and `extensions` modules wire up correctly end-to-end,
//! ensuring nothing was lost when initialization logic was moved out of
//! `main.rs` and `app.rs` into owning modules.
use std::sync::Arc;
use ironclaw::db::DatabaseHandles;
use ironclaw::secrets::{CreateSecretParams, SecretsCrypto, SecretsStore};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Build a libsql DatabaseConfig pointing at a temp file.
#[cfg(feature = "libsql")]
fn libsql_config(path: &std::path::Path) -> ironclaw::config::DatabaseConfig {
ironclaw::config::DatabaseConfig {
backend: ironclaw::config::DatabaseBackend::LibSql,
url: secrecy::SecretString::from(String::new()),
pool_size: 1,
ssl_mode: ironclaw::config::SslMode::Prefer,
libsql_path: Some(path.to_path_buf()),
libsql_url: None,
libsql_auth_token: None,
}
}
/// Build a master-key crypto instance for tests.
fn test_crypto() -> Arc<SecretsCrypto> {
let key = secrecy::SecretString::from(ironclaw::secrets::keychain::generate_master_key_hex());
Arc::new(SecretsCrypto::new(key).expect("test crypto"))
}
// ---------------------------------------------------------------------------
// connect_with_handles: returns Database + populated handles
// ---------------------------------------------------------------------------
#[cfg(feature = "libsql")]
#[tokio::test]
async fn connect_with_handles_returns_db_and_libsql_handle() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("test.db");
let config = libsql_config(&db_path);
let (db, handles) = ironclaw::db::connect_with_handles(&config)
.await
.expect("connect_with_handles");
// Database trait object works — run a trivial operation.
db.run_migrations().await.expect("migrations");
// Handle is populated.
assert!(
handles.libsql_db.is_some(),
"libsql handle should be Some after connect_with_handles"
);
}
// ---------------------------------------------------------------------------
// connect_from_config delegates to connect_with_handles
// ---------------------------------------------------------------------------
#[cfg(feature = "libsql")]
#[tokio::test]
async fn connect_from_config_produces_working_db() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("test.db");
let config = libsql_config(&db_path);
// connect_from_config delegates to connect_with_handles internally.
let db = ironclaw::db::connect_from_config(&config)
.await
.expect("connect_from_config");
// Verify usable — migrations should be idempotent.
db.run_migrations().await.expect("migrations");
}
// ---------------------------------------------------------------------------
// secrets::create_secrets_store from DatabaseHandles
// ---------------------------------------------------------------------------
#[cfg(feature = "libsql")]
#[tokio::test]
async fn secrets_store_from_handles_round_trips() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("test.db");
let config = libsql_config(&db_path);
let (_db, handles) = ironclaw::db::connect_with_handles(&config)
.await
.expect("connect");
let crypto = test_crypto();
let store = ironclaw::secrets::create_secrets_store(crypto, &handles)
.expect("create_secrets_store should return Some for libsql");
// Round-trip a secret to prove the store works.
store
.create("test", CreateSecretParams::new("test_key", "test_value"))
.await
.expect("create secret");
let decrypted = store
.get_decrypted("test", "test_key")
.await
.expect("get_decrypted");
assert_eq!(decrypted.expose(), "test_value");
}
// ---------------------------------------------------------------------------
// db::create_secrets_store (standalone CLI factory)
// ---------------------------------------------------------------------------
#[cfg(feature = "libsql")]
#[tokio::test]
async fn db_create_secrets_store_standalone_round_trips() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("test.db");
let config = libsql_config(&db_path);
let crypto = test_crypto();
let store = ironclaw::db::create_secrets_store(&config, crypto)
.await
.expect("db::create_secrets_store");
store
.create(
"test",
CreateSecretParams::new("standalone_key", "standalone_value"),
)
.await
.expect("create secret");
let decrypted = store
.get_decrypted("test", "standalone_key")
.await
.expect("get_decrypted");
assert_eq!(decrypted.expose(), "standalone_value");
}
// ---------------------------------------------------------------------------
// Both secrets factories produce equivalent stores
// ---------------------------------------------------------------------------
#[cfg(feature = "libsql")]
#[tokio::test]
async fn both_secrets_factories_produce_compatible_stores() {
let dir = tempfile::tempdir().expect("tempdir");
let db_path = dir.path().join("test.db");
let config = libsql_config(&db_path);
let crypto = test_crypto();
// Factory 1: connect_with_handles + secrets::create_secrets_store
let (_db, handles) = ironclaw::db::connect_with_handles(&config)
.await
.expect("connect");
let store_a = ironclaw::secrets::create_secrets_store(Arc::clone(&crypto), &handles)
.expect("store from handles");
// Factory 2: db::create_secrets_store (standalone)
let store_b = ironclaw::db::create_secrets_store(&config, crypto)
.await
.expect("standalone store");
// Write with factory 1, read with factory 2.
store_a
.create(
"test",
CreateSecretParams::new("cross_factory", "shared_secret"),
)
.await
.expect("create via store_a");
let decrypted = store_b
.get_decrypted("test", "cross_factory")
.await
.expect("read via store_b");
assert_eq!(decrypted.expose(), "shared_secret");
}
// ---------------------------------------------------------------------------
// ExtensionManager constructs with McpProcessManager
// ---------------------------------------------------------------------------
#[tokio::test]
async fn extension_manager_with_process_manager_constructs() {
use ironclaw::extensions::ExtensionManager;
use ironclaw::secrets::InMemorySecretsStore;
use ironclaw::tools::ToolRegistry;
use ironclaw::tools::mcp::McpProcessManager;
use ironclaw::tools::mcp::McpSessionManager;
let crypto = test_crypto();
let secrets: Arc<dyn SecretsStore + Send + Sync> = Arc::new(InMemorySecretsStore::new(crypto));
let tools = Arc::new(ToolRegistry::new());
let tools_dir = tempfile::tempdir().expect("tools_dir");
let channels_dir = tempfile::tempdir().expect("channels_dir");
let manager = ExtensionManager::new(
Arc::new(McpSessionManager::new()),
Arc::new(McpProcessManager::new()),
secrets,
tools,
None,
None,
tools_dir.path().to_path_buf(),
channels_dir.path().to_path_buf(),
None,
"test".to_string(),
None,
Vec::new(),
);
// Verify the manager is functional — list returns Ok.
let result = manager.list(None, false).await;
assert!(result.is_ok(), "list should succeed on empty manager");
assert!(result.unwrap().is_empty());
}
// ---------------------------------------------------------------------------
// DatabaseHandles: default is empty
// ---------------------------------------------------------------------------
#[test]
fn database_handles_default_is_empty() {
let handles = DatabaseHandles::default();
#[cfg(feature = "postgres")]
assert!(handles.pg_pool.is_none());
#[cfg(feature = "libsql")]
assert!(handles.libsql_db.is_none());
}