diff --git a/.github/workflows/ci-firecracker.yml b/.github/workflows/ci-firecracker.yml index 01c8cdb0d..bfd9d9d55 100644 --- a/.github/workflows/ci-firecracker.yml +++ b/.github/workflows/ci-firecracker.yml @@ -42,10 +42,30 @@ jobs: - name: Create VM id: vm run: | - RESPONSE=$(curl -sf -X POST $FCCTL_URL/api/vms \ + RESPONSE_FILE=$(mktemp) + trap 'rm -f "${RESPONSE_FILE}"' EXIT + if HTTP_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w "%{http_code}" -X POST "$FCCTL_URL/api/vms" \ -H 'Content-Type: application/json' \ - -d "{\"vm_type\": \"$VM_TYPE\"}") + -d "{\"vm_type\": \"$VM_TYPE\"}"); then + : + else + CURL_EXIT=$? + RESPONSE=$(cat "$RESPONSE_FILE") + echo "fcctl create transport failed: curl exit $CURL_EXIT" + if [ -n "$RESPONSE" ]; then + echo "$RESPONSE" + fi + exit 1 + fi + RESPONSE=$(cat "$RESPONSE_FILE") + rm -f "$RESPONSE_FILE" + trap - EXIT + echo "fcctl create status: $HTTP_STATUS" echo "$RESPONSE" + if [ "$HTTP_STATUS" -lt 200 ] || [ "$HTTP_STATUS" -ge 300 ]; then + echo "ERROR: fcctl-web VM create failed with HTTP $HTTP_STATUS" + exit 1 + fi VM_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") if [ -z "$VM_ID" ] || [ "$VM_ID" = "null" ]; then echo "ERROR: failed to parse vm id from response" @@ -144,8 +164,8 @@ jobs: # bigbox; see .cargo/config.toml for the Darwin `dynamic_lookup` # linker workaround on local Mac dev. # - # All cargo invocations are dispatched via `rch exec --` so they - # share rchd's queue + slot accounting with ADF agents (see + # Build-oriented cargo invocations are dispatched via `rch exec --` so + # they share rchd's queue + slot accounting with ADF agents (see # .docs/adr-rch-build-queue-not-firecracker-ci.md). Fail-open: if # rchd is down or no slot is available, rch falls through to local # cargo with no behaviour change. @@ -161,15 +181,34 @@ jobs: - name: Install cargo-nextest run: | if ! command -v cargo-nextest >/dev/null 2>&1; then - cargo install cargo-nextest --locked + curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "${CARGO_HOME:-$HOME/.cargo}/bin" fi cargo nextest --version - name: cargo nextest run --workspace - # Only test_chat_command is skipped: it requires LLM API credentials - # not present in CI. All other failures must be fixed at the source, - # not skipped. nextest uses filter expressions instead of --skip. - run: /home/alex/.local/bin/rch exec -- cargo nextest run --workspace --profile ci -E 'not test(test_chat_command)' + # test_chat_command is skipped: it requires LLM API credentials not + # present in CI. The MCP autocomplete E2E binary is excluded here only + # because it spawns nested `cargo run`; the following step covers it + # serially to avoid build-dir lock contention. All other failures must + # be fixed at the source, not skipped. nextest uses filter expressions + # instead of --skip. + # Do not wrap these invocations in `rch exec`: rch's non-compilation + # command path loses shell quoting around filter expressions and makes + # `/bin/sh` parse parentheses in predicates like `test(...)`. + # `RUSTC_WRAPPER=sccache` above still applies to rustc invocations. + run: | + cargo nextest run --workspace --profile ci \ + -E 'not test(test_chat_command) - binary_id(terraphim_mcp_server::mcp_autocomplete_e2e_test)' + + - name: cargo nextest run mcp_autocomplete_e2e serially + # These tests spawn `cargo run --bin terraphim_mcp_server`. Under + # nextest each test is its own process, so `#[serial]` cannot prevent + # concurrent nested Cargo invocations. Run this binary separately with + # one test thread to avoid build-dir lock contention and CI timeouts. + run: | + cargo nextest run -p terraphim_mcp_server --test mcp_autocomplete_e2e_test \ + --profile ci --test-threads 1 --no-capture \ + -E 'not test(test_chat_command)' - name: sccache stats if: always() diff --git a/.github/workflows/performance-benchmarking.yml b/.github/workflows/performance-benchmarking.yml index 539727743..5a8075f0d 100644 --- a/.github/workflows/performance-benchmarking.yml +++ b/.github/workflows/performance-benchmarking.yml @@ -38,7 +38,7 @@ jobs: performance-benchmarks: name: Performance Benchmarks runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 45 steps: - name: Checkout code diff --git a/crates/terraphim_orchestrator/src/lib_tests.rs b/crates/terraphim_orchestrator/src/lib_tests.rs index a809715a2..78663ca71 100644 --- a/crates/terraphim_orchestrator/src/lib_tests.rs +++ b/crates/terraphim_orchestrator/src/lib_tests.rs @@ -193,11 +193,14 @@ async fn test_direct_dispatch_config_starts_socket_listener() { #[tokio::test] async fn test_handle_direct_dispatch_spawns_agent_without_mentions() { + let temp = TempDir::new().unwrap(); let mut config = test_config(); + config.working_dir = temp.path().to_path_buf(); + config.compound_review.worktree_root = temp.path().join(".worktrees"); config.agents = vec![AgentDefinition { name: "echo-agent".to_string(), layer: AgentLayer::Core, - cli_tool: "echo".to_string(), + cli_tool: "/bin/echo".to_string(), task: "echo hello".to_string(), schedule: None, model: None, diff --git a/crates/terraphim_rlm/src/main.rs b/crates/terraphim_rlm/src/main.rs index 64a876d4a..0552b5816 100644 --- a/crates/terraphim_rlm/src/main.rs +++ b/crates/terraphim_rlm/src/main.rs @@ -48,7 +48,10 @@ async fn run(cli: Cli) -> Result> { #[cfg(feature = "llm")] { if let Err(e) = rlm.auto_configure_llm().await { - log::warn!("LLM auto-configuration failed: {}. rlm_query will be unavailable.", e); + log::warn!( + "LLM auto-configuration failed: {}. rlm_query will be unavailable.", + e + ); } } diff --git a/crates/terraphim_rlm/src/query_loop.rs b/crates/terraphim_rlm/src/query_loop.rs index 5afe12901..277e03375 100644 --- a/crates/terraphim_rlm/src/query_loop.rs +++ b/crates/terraphim_rlm/src/query_loop.rs @@ -690,7 +690,12 @@ fn truncate(s: &str, max_len: usize) -> String { if s.len() <= max_len { s.to_string() } else { - let boundary = s.floor_char_boundary(max_len); + let boundary = s + .char_indices() + .map(|(idx, _)| idx) + .take_while(|idx| *idx <= max_len) + .last() + .unwrap_or(0); format!("{}...", &s[..boundary]) } } diff --git a/crates/terraphim_rlm/src/rlm.rs b/crates/terraphim_rlm/src/rlm.rs index e7c5554d7..1f304855b 100644 --- a/crates/terraphim_rlm/src/rlm.rs +++ b/crates/terraphim_rlm/src/rlm.rs @@ -910,8 +910,7 @@ impl TerraphimRlm { "llm_provider".to_string(), serde_json::Value::String("ollama".to_string()), ); - let ollama_model = std::env::var("RLM_MODEL") - .unwrap_or_else(|_| "gemma3:270m".to_string()); + let ollama_model = std::env::var("RLM_MODEL").unwrap_or_else(|_| "gemma3:270m".to_string()); role.extra.insert( "llm_model".to_string(), serde_json::Value::String(ollama_model.clone()), @@ -929,7 +928,9 @@ impl TerraphimRlm { ]) .output() .ok()?; - String::from_utf8(output.stdout).ok().map(|s| s.trim().to_string()) + String::from_utf8(output.stdout) + .ok() + .map(|s| s.trim().to_string()) }); if let Some(ref key) = or_api_key { @@ -938,7 +939,9 @@ impl TerraphimRlm { role.llm_api_key = Some(key.clone()); role.llm_model = Some(or_model.clone()); // Cache for child processes and build_llm_from_role - unsafe { std::env::set_var("OPENROUTER_API_KEY", key); } + unsafe { + std::env::set_var("OPENROUTER_API_KEY", key); + } log::info!("RLM auto-configure: openrouter model={}", or_model); } @@ -958,11 +961,10 @@ impl TerraphimRlm { ..Default::default() }); - let client = terraphim_service::llm::build_llm_from_role(&role) - .ok_or_else(|| { - log::warn!("RLM auto-configure: no LLM provider available"); - RlmError::LlmNotConfigured - })?; + let client = terraphim_service::llm::build_llm_from_role(&role).ok_or_else(|| { + log::warn!("RLM auto-configure: no LLM provider available"); + RlmError::LlmNotConfigured + })?; log::info!( "RLM LLM bridge configured: providers={} strategy={:?}", diff --git a/crates/terraphim_rlm/tests/backend_demo.rs b/crates/terraphim_rlm/tests/backend_demo.rs index 76ddaf3d9..18de607e3 100644 --- a/crates/terraphim_rlm/tests/backend_demo.rs +++ b/crates/terraphim_rlm/tests/backend_demo.rs @@ -1,8 +1,8 @@ //! Demonstration: RLM running locally (LocalExecutor) and via Docker (DockerExecutor). //! Run: cargo test -p terraphim_rlm --test backend_demo -- --nocapture -use terraphim_rlm::config::{BackendType, RlmConfig}; use terraphim_rlm::TerraphimRlm; +use terraphim_rlm::config::{BackendType, RlmConfig}; #[tokio::test] async fn demo_local_executor() { @@ -19,11 +19,22 @@ async fn demo_local_executor() { // Python let r = rlm.execute_code(&session.id, "print(2+2)").await.unwrap(); - println!(" [Python] 2+2 = {} (exit {})", r.stdout.trim(), r.exit_code); + println!( + " [Python] 2+2 = {} (exit {})", + r.stdout.trim(), + r.exit_code + ); // Bash - let r = rlm.execute_command(&session.id, "echo hello-from-local").await.unwrap(); - println!(" [Bash] echo = {} (exit {})", r.stdout.trim(), r.exit_code); + let r = rlm + .execute_command(&session.id, "echo hello-from-local") + .await + .unwrap(); + println!( + " [Bash] echo = {} (exit {})", + r.stdout.trim(), + r.exit_code + ); // Show backend type let status = rlm.get_session_status(&session.id, false).await.unwrap(); @@ -55,11 +66,22 @@ async fn demo_docker_executor() { // Python let r = rlm.execute_code(&session.id, "print(2+2)").await.unwrap(); - println!(" [Python] 2+2 = {} (exit {})", r.stdout.trim(), r.exit_code); + println!( + " [Python] 2+2 = {} (exit {})", + r.stdout.trim(), + r.exit_code + ); // Bash - let r = rlm.execute_command(&session.id, "echo hello-from-docker").await.unwrap(); - println!(" [Bash] echo = {} (exit {})", r.stdout.trim(), r.exit_code); + let r = rlm + .execute_command(&session.id, "echo hello-from-docker") + .await + .unwrap(); + println!( + " [Bash] echo = {} (exit {})", + r.stdout.trim(), + r.exit_code + ); // Show backend type let status = rlm.get_session_status(&session.id, false).await.unwrap(); @@ -73,11 +95,17 @@ async fn demo_docker_executor() { println!(" [Container hostname] {}", r.stdout.trim()); // Show Python version inside container - let r = rlm.execute_code(&session.id, "import sys; print(sys.version)").await.unwrap(); + let r = rlm + .execute_code(&session.id, "import sys; print(sys.version)") + .await + .unwrap(); println!(" [Python version] {}", r.stdout.trim()); // Show container filesystem - let r = rlm.execute_command(&session.id, "ls / | head -5").await.unwrap(); + let r = rlm + .execute_command(&session.id, "ls / | head -5") + .await + .unwrap(); println!(" [Container root]\n{}", r.stdout); rlm.destroy_session(&session.id).await.unwrap(); diff --git a/crates/terraphim_rlm/tests/skills_demo.rs b/crates/terraphim_rlm/tests/skills_demo.rs index cbf439f9e..f922a2d82 100644 --- a/crates/terraphim_rlm/tests/skills_demo.rs +++ b/crates/terraphim_rlm/tests/skills_demo.rs @@ -6,21 +6,17 @@ use terraphim_rlm::{RlmConfig, TerraphimRlm}; #[tokio::test] async fn demo_all_skills() { let config = RlmConfig::minimal(); - let rlm = TerraphimRlm::with_executor( - config, - terraphim_rlm::LocalExecutor::new(), - ) - .unwrap(); + let rlm = TerraphimRlm::with_executor(config, terraphim_rlm::LocalExecutor::new()).unwrap(); // 1. Session create let session = rlm.create_session().await.unwrap(); - println!("[session create] id={} state={:?}", session.id, session.state); + println!( + "[session create] id={} state={:?}", + session.id, session.state + ); // 2. Code execution - let result = rlm - .execute_code(&session.id, "print(2+2)") - .await - .unwrap(); + let result = rlm.execute_code(&session.id, "print(2+2)").await.unwrap(); println!( "[code: 2+2] exit={} stdout={:?}", result.exit_code, result.stdout @@ -64,10 +60,7 @@ async fn demo_all_skills() { assert_eq!(val, None); // 8. Status - let status = rlm - .get_session_status(&session.id, false) - .await - .unwrap(); + let status = rlm.get_session_status(&session.id, false).await.unwrap(); println!( "[status] backend={:?} snapshots={}", status.backend_type, status.snapshot_count diff --git a/terraphim_server/tests/api_context_tests.rs b/terraphim_server/tests/api_context_tests.rs index 76a76347b..a27b27aa6 100644 --- a/terraphim_server/tests/api_context_tests.rs +++ b/terraphim_server/tests/api_context_tests.rs @@ -14,7 +14,6 @@ use terraphim_server::{ GetConversationResponse, ListConversationsResponse, Status, axum_server, }; use terraphim_service::http_client; -use terraphim_settings::DeviceSettings; use terraphim_types::{ContextType, Document, DocumentType, RelevanceFunction}; /// Sample configuration for testing context management @@ -50,15 +49,8 @@ fn create_test_config() -> Config { /// Start a test server with context management API async fn start_test_server() -> SocketAddr { - let server_settings = - DeviceSettings::load_from_env_and_file(None).expect("Failed to load settings"); - let server_hostname = server_settings - .server_hostname - .parse::() - .unwrap_or_else(|_| { - let port = portpicker::pick_unused_port().expect("Failed to find unused port"); - SocketAddr::from(([127, 0, 0, 1], port)) - }); + let port = portpicker::pick_unused_port().expect("Failed to find unused port"); + let server_hostname = SocketAddr::from(([127, 0, 0, 1], port)); let mut config = create_test_config(); let config_state = terraphim_config::ConfigState::new(&mut config)