diff --git a/crates/arc-agent/src/cli.rs b/crates/arc-agent/src/cli.rs index 27076f279..09446c31b 100644 --- a/crates/arc-agent/src/cli.rs +++ b/crates/arc-agent/src/cli.rs @@ -87,7 +87,10 @@ impl AgentArgs { .or_else(|| provider.map(String::from)) .or_else(|| Some("anthropic".to_string())); self.model = self.model.take().or_else(|| model.map(String::from)); - self.permissions = self.permissions.or(permissions).or(Some(PermissionLevel::ReadWrite)); + self.permissions = self + .permissions + .or(permissions) + .or(Some(PermissionLevel::ReadWrite)); self.output_format = self .output_format .or(output_format) @@ -381,10 +384,10 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> { // Resolve model and build profile let model = args.model.unwrap_or_else(|| { - arc_llm::catalog::default_model_for_provider(provider.as_str()) - .map(|m| m.id) - .unwrap_or_else(|| provider.as_str().to_string()) - }); + arc_llm::catalog::default_model_for_provider(provider.as_str()) + .map(|m| m.id) + .unwrap_or_else(|| provider.as_str().to_string()) + }); eprintln!("{}", styles.dim.apply_to(format!("Using model: {model}"))); let mut profile = build_profile(provider, &model, Some(client.clone())); @@ -523,7 +526,11 @@ pub async fn run_with_args(args: AgentArgs) -> anyhow::Result<()> { .. } => { let short_id = &agent_id[..8.min(agent_id.len())]; - let task_preview = if task.len() > 60 { &task[..crate::truncation::floor_char_boundary(task, 60)] } else { task }; + let task_preview = if task.len() > 60 { + &task[..crate::truncation::floor_char_boundary(task, 60)] + } else { + task + }; eprintln!( " {}", s.dim.apply_to(format!( diff --git a/crates/arc-agent/src/compaction.rs b/crates/arc-agent/src/compaction.rs index 9e69ec9ee..7c59493da 100644 --- a/crates/arc-agent/src/compaction.rs +++ b/crates/arc-agent/src/compaction.rs @@ -197,7 +197,10 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String { for tc in tool_calls { let args_str = tc.arguments.to_string(); let truncated = if args_str.len() > 500 { - format!("{}...", &args_str[..crate::truncation::floor_char_boundary(&args_str, 500)]) + format!( + "{}...", + &args_str[..crate::truncation::floor_char_boundary(&args_str, 500)] + ) } else { args_str }; @@ -208,7 +211,11 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String { for r in results { let content_str = r.content.to_string(); let truncated = if content_str.len() > 500 { - format!("{}...", &content_str[..crate::truncation::floor_char_boundary(&content_str, 500)]) + format!( + "{}...", + &content_str + [..crate::truncation::floor_char_boundary(&content_str, 500)] + ) } else { content_str }; diff --git a/crates/arc-agent/src/docker_sandbox.rs b/crates/arc-agent/src/docker_sandbox.rs index 304341ca5..5e5e7076d 100644 --- a/crates/arc-agent/src/docker_sandbox.rs +++ b/crates/arc-agent/src/docker_sandbox.rs @@ -748,10 +748,7 @@ impl Sandbox for DockerSandbox { } fn sandbox_info(&self) -> String { - self.container_id - .get() - .cloned() - .unwrap_or_default() + self.container_id.get().cloned().unwrap_or_default() } } diff --git a/crates/arc-agent/src/history.rs b/crates/arc-agent/src/history.rs index 0638f6193..693a15687 100644 --- a/crates/arc-agent/src/history.rs +++ b/crates/arc-agent/src/history.rs @@ -442,7 +442,10 @@ mod tests { .. } = assistant_turn { - assert!(provider_parts.is_empty(), "reasoning items should be stripped"); + assert!( + provider_parts.is_empty(), + "reasoning items should be stripped" + ); assert_eq!(tool_calls.len(), 1, "tool_calls should be preserved"); assert_eq!(content, "response", "text content should be preserved"); } else { @@ -479,7 +482,11 @@ mod tests { let assistant_turn = &history.turns()[2]; if let Turn::Assistant { provider_parts, .. } = assistant_turn { - assert_eq!(provider_parts.len(), 1, "thinking block should be preserved"); + assert_eq!( + provider_parts.len(), + 1, + "thinking block should be preserved" + ); assert!(matches!(&provider_parts[0], ContentPart::Thinking(_))); } else { panic!("expected Assistant turn"); diff --git a/crates/arc-agent/src/tools.rs b/crates/arc-agent/src/tools.rs index 6c5623637..9ab91a671 100644 --- a/crates/arc-agent/src/tools.rs +++ b/crates/arc-agent/src/tools.rs @@ -219,10 +219,19 @@ pub fn make_shell_tool_with_config(config: &SessionConfig) -> RegisteredTool { .unwrap_or(default_timeout) .min(max_timeout); - tracing::debug!(env_var_count = ctx.tool_env.as_ref().map_or(0, |e| e.len()), "Injecting sandbox env vars into tool execution"); + tracing::debug!( + env_var_count = ctx.tool_env.as_ref().map_or(0, |e| e.len()), + "Injecting sandbox env vars into tool execution" + ); let result = ctx .env - .exec_command(command, timeout_ms, None, ctx.tool_env.as_ref(), Some(ctx.cancel)) + .exec_command( + command, + timeout_ms, + None, + ctx.tool_env.as_ref(), + Some(ctx.cancel), + ) .await?; let mut output = String::new(); diff --git a/crates/arc-agent/src/v4a_patch.rs b/crates/arc-agent/src/v4a_patch.rs index 0bc5462ad..4aabd0bda 100644 --- a/crates/arc-agent/src/v4a_patch.rs +++ b/crates/arc-agent/src/v4a_patch.rs @@ -20,8 +20,13 @@ pub struct Hunk { #[derive(Debug, Clone, PartialEq, Eq)] pub enum PatchOperation { - Add { path: String, content: String }, - Delete { path: String }, + Add { + path: String, + content: String, + }, + Delete { + path: String, + }, Update { path: String, new_path: Option, @@ -156,13 +161,12 @@ pub fn parse_v4a_patch(text: &str) -> Result, String> { } // Check for *** End of File marker - let end_of_file = - if i < lines.len() && lines[i].trim() == "*** End of File" { - i += 1; - true - } else { - false - }; + let end_of_file = if i < lines.len() && lines[i].trim() == "*** End of File" { + i += 1; + true + } else { + false + }; hunks.push(Hunk { context_line, @@ -309,9 +313,7 @@ fn apply_hunks(content: &str, hunks: &[Hunk]) -> Result { Change::Remove(t) | Change::Context(t) => Some(t.as_str()), Change::Add(_) => None, }) - .ok_or( - "Hunk with bare @@ has no remove or context lines to locate position", - )?; + .ok_or("Hunk with bare @@ has no remove or context lines to locate position")?; find_line_match_reverse(&lines, first_match_text) } .ok_or_else(|| { @@ -660,8 +662,14 @@ mod tests { assert_eq!(hunks.len(), 1); assert_eq!(hunks[0].context_line, ""); assert_eq!(hunks[0].changes.len(), 4); - assert_eq!(hunks[0].changes[0], Change::Context("fn unchanged() {".into())); - assert_eq!(hunks[0].changes[1], Change::Remove(" old_line();".into())); + assert_eq!( + hunks[0].changes[0], + Change::Context("fn unchanged() {".into()) + ); + assert_eq!( + hunks[0].changes[1], + Change::Remove(" old_line();".into()) + ); assert_eq!(hunks[0].changes[2], Change::Add(" new_line();".into())); assert_eq!(hunks[0].changes[3], Change::Context("}".into())); } @@ -981,10 +989,7 @@ mod tests { }]; let result = apply_hunks(content, &hunks).unwrap(); // First "pass" should be untouched, second should be replaced - assert_eq!( - result, - "def foo():\n pass\n\ndef bar():\n return 99" - ); + assert_eq!(result, "def foo():\n pass\n\ndef bar():\n return 99"); } // Phase 4: *** Move to: @@ -1447,14 +1452,12 @@ def farewell(name): let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::with_tools(registry)); - let mut session = Session::new( - client, - profile, - env.clone(), - SessionConfig::default(), - ); + let mut session = Session::new(client, profile, env.clone(), SessionConfig::default()); session.initialize().await; - session.process_input("Update the greeting functions").await.unwrap(); + session + .process_input("Update the greeting functions") + .await + .unwrap(); let content = env.read_file("src/app.py", None, None).await.unwrap(); assert!(content.contains("Hello, {name}!")); diff --git a/crates/arc-agent/tests/guardrails.rs b/crates/arc-agent/tests/guardrails.rs index 67cc21123..e1f97d291 100644 --- a/crates/arc-agent/tests/guardrails.rs +++ b/crates/arc-agent/tests/guardrails.rs @@ -6,9 +6,7 @@ use arc_llm::provider::Provider; fn profile_context_window_matches_catalog_for_default_models() { for &provider in Provider::ALL { let catalog_info = catalog::default_model_for_provider(provider.as_str()) - .unwrap_or_else(|| { - panic!("no default model for {:?} in catalog", provider) - }); + .unwrap_or_else(|| panic!("no default model for {:?} in catalog", provider)); let model = &catalog_info.id; let profile: Box = match provider { diff --git a/crates/arc-agent/tests/parity_matrix.rs b/crates/arc-agent/tests/parity_matrix.rs index f764cf55b..6f4d65d00 100644 --- a/crates/arc-agent/tests/parity_matrix.rs +++ b/crates/arc-agent/tests/parity_matrix.rs @@ -120,9 +120,19 @@ macro_rules! provider_test { macro_rules! provider_tests { ($scenario:ident) => { - provider_test!($scenario, Provider::Anthropic, "claude-haiku-4-5", anthropic); + provider_test!( + $scenario, + Provider::Anthropic, + "claude-haiku-4-5", + anthropic + ); provider_test!($scenario, Provider::OpenAi, "gpt-5-mini", openai); - provider_test!($scenario, Provider::Gemini, "gemini-3-flash-preview", gemini); + provider_test!( + $scenario, + Provider::Gemini, + "gemini-3-flash-preview", + gemini + ); provider_test!($scenario, Provider::Kimi, "kimi-k2.5", kimi); provider_test!($scenario, Provider::Zai, "glm-4.7", zai); provider_test!($scenario, Provider::Minimax, "minimax-m2.5", minimax); @@ -157,8 +167,18 @@ provider_tests!(error_recovery); // gpt-5-mini is too weak to reliably apply precise file edits (uses apply_patch, not edit_file). macro_rules! non_openai_provider_tests { ($scenario:ident) => { - provider_test!($scenario, Provider::Anthropic, "claude-haiku-4-5", anthropic); - provider_test!($scenario, Provider::Gemini, "gemini-3-flash-preview", gemini); + provider_test!( + $scenario, + Provider::Anthropic, + "claude-haiku-4-5", + anthropic + ); + provider_test!( + $scenario, + Provider::Gemini, + "gemini-3-flash-preview", + gemini + ); provider_test!($scenario, Provider::Kimi, "kimi-k2.5", kimi); provider_test!($scenario, Provider::Zai, "glm-4.7", zai); provider_test!($scenario, Provider::Minimax, "minimax-m2.5", minimax); @@ -399,9 +419,17 @@ macro_rules! reasoning_effort_tests { }; } -reasoning_effort_tests!(Provider::Anthropic, "claude-haiku-4-5", anthropic_reasoning_effort); +reasoning_effort_tests!( + Provider::Anthropic, + "claude-haiku-4-5", + anthropic_reasoning_effort +); // gpt-5-mini does not support the reasoning.effort parameter, so no OpenAI test. -reasoning_effort_tests!(Provider::Gemini, "gemini-3-flash-preview", gemini_reasoning_effort); +reasoning_effort_tests!( + Provider::Gemini, + "gemini-3-flash-preview", + gemini_reasoning_effort +); reasoning_effort_tests!(Provider::Kimi, "kimi-k2.5", kimi_reasoning_effort); reasoning_effort_tests!(Provider::Zai, "glm-4.7", zai_reasoning_effort); reasoning_effort_tests!(Provider::Minimax, "minimax-m2.5", minimax_reasoning_effort); @@ -445,9 +473,17 @@ macro_rules! loop_detection_tests { }; } -loop_detection_tests!(Provider::Anthropic, "claude-haiku-4-5", anthropic_loop_detection); +loop_detection_tests!( + Provider::Anthropic, + "claude-haiku-4-5", + anthropic_loop_detection +); loop_detection_tests!(Provider::OpenAi, "gpt-5-mini", openai_loop_detection); -loop_detection_tests!(Provider::Gemini, "gemini-3-flash-preview", gemini_loop_detection); +loop_detection_tests!( + Provider::Gemini, + "gemini-3-flash-preview", + gemini_loop_detection +); loop_detection_tests!(Provider::Kimi, "kimi-k2.5", kimi_loop_detection); loop_detection_tests!(Provider::Zai, "glm-4.7", zai_loop_detection); loop_detection_tests!(Provider::Minimax, "minimax-m2.5", minimax_loop_detection); diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index a174d464b..3050d4562 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -4,10 +4,10 @@ use std::sync::Arc; use axum::extract::{Path, Query, State}; -use serde_json::json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; +use serde_json::json; use crate::error::ApiError; use crate::jwt_auth::AuthenticatedService; @@ -23,13 +23,20 @@ pub struct RetroListParams { smoothness: Option, } -fn paginated_response(items: Vec, pagination: &PaginationParams) -> Response { +fn paginated_response( + items: Vec, + pagination: &PaginationParams, +) -> Response { let limit = pagination.limit.clamp(1, 100) as usize; let offset = pagination.offset as usize; let mut data: Vec<_> = items.into_iter().skip(offset).take(limit + 1).collect(); let has_more = data.len() > limit; data.truncate(limit); - (StatusCode::OK, Json(json!({ "data": data, "meta": { "has_more": has_more } }))).into_response() + ( + StatusCode::OK, + Json(json!({ "data": data, "meta": { "has_more": has_more } })), + ) + .into_response() } // ── Runs ─────────────────────────────────────────────────────────────── @@ -218,7 +225,11 @@ pub async fn get_run_graph( { Ok(child) => child, Err(_) => { - return ApiError::new(StatusCode::BAD_GATEWAY, "Graphviz dot command not available.").into_response(); + return ApiError::new( + StatusCode::BAD_GATEWAY, + "Graphviz dot command not available.", + ) + .into_response(); } }; @@ -332,10 +343,26 @@ pub async fn list_retros( ) -> Response { let items: Vec<_> = retros::list_items() .into_iter() - .filter(|r| params.workflow.as_ref().map_or(true, |w| &r.workflow.slug == w)) - .filter(|r| params.smoothness.as_ref().map_or(true, |s| r.smoothness.as_ref() == Some(s))) + .filter(|r| { + params + .workflow + .as_ref() + .map_or(true, |w| &r.workflow.slug == w) + }) + .filter(|r| { + params + .smoothness + .as_ref() + .map_or(true, |s| r.smoothness.as_ref() == Some(s)) + }) .collect(); - paginated_response(items, &PaginationParams { limit: params.limit, offset: params.offset }) + paginated_response( + items, + &PaginationParams { + limit: params.limit, + offset: params.offset, + }, + ) } // ── Sessions ─────────────────────────────────────────────────────────── @@ -377,7 +404,11 @@ pub async fn send_message_stub( State(_state): State>, Path(_id): Path, ) -> Response { - (StatusCode::ACCEPTED, Json(serde_json::json!({"accepted": true}))).into_response() + ( + StatusCode::ACCEPTED, + Json(serde_json::json!({"accepted": true})), + ) + .into_response() } pub async fn session_events_stub( @@ -407,9 +438,7 @@ pub async fn session_events_stub( arc_types::SessionTurn::AssistantTurn(t) => { ("assistant_turn", serde_json::to_string(t).unwrap()) } - arc_types::SessionTurn::ToolTurn(t) => { - ("tool_turn", serde_json::to_string(t).unwrap()) - } + arc_types::SessionTurn::ToolTurn(t) => ("tool_turn", serde_json::to_string(t).unwrap()), }; if last_event_id.is_none() || seq > last_event_id.unwrap() { @@ -552,69 +581,139 @@ mod runs { vec![ RunListItem { id: "run-1".into(), - repository: RepositoryReference { name: "api-server".into() }, + repository: RepositoryReference { + name: "api-server".into(), + }, title: "Add rate limiting to auth endpoints".into(), - workflow: WorkflowReference { slug: "implement".into() }, + workflow: WorkflowReference { + slug: "implement".into(), + }, status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { elapsed_secs: 420.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-a1b2c3d4".into(), resources: Some(SandboxResources { cpu: 4, memory: 8 }) }), + timings: Some(RunTimings { + elapsed_secs: 420.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-a1b2c3d4".into(), + resources: Some(SandboxResources { cpu: 4, memory: 8 }), + }), question: None, created_at: ts("2026-03-06T14:30:00Z"), }, RunListItem { id: "run-2".into(), - repository: RepositoryReference { name: "web-dashboard".into() }, + repository: RepositoryReference { + name: "web-dashboard".into(), + }, title: "Migrate to React Router v7".into(), - workflow: WorkflowReference { slug: "implement".into() }, + workflow: WorkflowReference { + slug: "implement".into(), + }, status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { elapsed_secs: 8100.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-e5f6g7h8".into(), resources: Some(SandboxResources { cpu: 8, memory: 16 }) }), + timings: Some(RunTimings { + elapsed_secs: 8100.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-e5f6g7h8".into(), + resources: Some(SandboxResources { cpu: 8, memory: 16 }), + }), question: None, created_at: ts("2026-03-06T12:00:00Z"), }, RunListItem { id: "run-3".into(), - repository: RepositoryReference { name: "cli-tools".into() }, + repository: RepositoryReference { + name: "cli-tools".into(), + }, title: "Fix config parsing for nested values".into(), - workflow: WorkflowReference { slug: "fix_build".into() }, + workflow: WorkflowReference { + slug: "fix_build".into(), + }, status: BoardColumn::Working, pull_request: None, - timings: Some(RunTimings { elapsed_secs: 2700.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-i9j0k1l2".into(), resources: Some(SandboxResources { cpu: 2, memory: 4 }) }), + timings: Some(RunTimings { + elapsed_secs: 2700.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-i9j0k1l2".into(), + resources: Some(SandboxResources { cpu: 2, memory: 4 }), + }), question: None, created_at: ts("2026-03-05T09:20:00Z"), }, RunListItem { id: "run-4".into(), - repository: RepositoryReference { name: "api-server".into() }, + repository: RepositoryReference { + name: "api-server".into(), + }, title: "Update OpenAPI spec for v3".into(), - workflow: WorkflowReference { slug: "expand".into() }, + workflow: WorkflowReference { + slug: "expand".into(), + }, status: BoardColumn::Pending, - pull_request: Some(RunPullRequest { number: 0, additions: Some(567), deletions: Some(234), comments: Some(0), checks: vec![] }), - timings: Some(RunTimings { elapsed_secs: 4320.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-q7r8s9t0".into(), resources: None }), - question: Some(RunQuestion { text: "Accept or push for another round?".into() }), + pull_request: Some(RunPullRequest { + number: 0, + additions: Some(567), + deletions: Some(234), + comments: Some(0), + checks: vec![], + }), + timings: Some(RunTimings { + elapsed_secs: 4320.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-q7r8s9t0".into(), + resources: None, + }), + question: Some(RunQuestion { + text: "Accept or push for another round?".into(), + }), created_at: ts("2026-03-04T15:00:00Z"), }, RunListItem { id: "run-5".into(), - repository: RepositoryReference { name: "shared-types".into() }, + repository: RepositoryReference { + name: "shared-types".into(), + }, title: "Add pipeline event types".into(), - workflow: WorkflowReference { slug: "implement".into() }, + workflow: WorkflowReference { + slug: "implement".into(), + }, status: BoardColumn::Pending, - pull_request: Some(RunPullRequest { number: 0, additions: Some(145), deletions: Some(23), comments: Some(0), checks: vec![] }), - timings: Some(RunTimings { elapsed_secs: 1680.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-u1v2w3x4".into(), resources: None }), - question: Some(RunQuestion { text: "Proceed from investigation to fix?".into() }), + pull_request: Some(RunPullRequest { + number: 0, + additions: Some(145), + deletions: Some(23), + comments: Some(0), + checks: vec![], + }), + timings: Some(RunTimings { + elapsed_secs: 1680.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-u1v2w3x4".into(), + resources: None, + }), + question: Some(RunQuestion { + text: "Proceed from investigation to fix?".into(), + }), created_at: ts("2026-03-04T10:00:00Z"), }, RunListItem { id: "run-6".into(), - repository: RepositoryReference { name: "web-dashboard".into() }, + repository: RepositoryReference { + name: "web-dashboard".into(), + }, title: "Add dark mode toggle".into(), - workflow: WorkflowReference { slug: "implement".into() }, + workflow: WorkflowReference { + slug: "implement".into(), + }, status: BoardColumn::Review, pull_request: Some(RunPullRequest { number: 889, @@ -622,25 +721,63 @@ mod runs { deletions: Some(67), comments: Some(4), checks: vec![ - CheckRun { name: "lint".into(), status: CheckRunStatus::Success, duration_secs: Some(23.0) }, - CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(72.0) }, - CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(154.0) }, - CheckRun { name: "integration-tests".into(), status: CheckRunStatus::Failure, duration_secs: Some(296.0) }, - CheckRun { name: "e2e / chrome".into(), status: CheckRunStatus::Failure, duration_secs: Some(182.0) }, - CheckRun { name: "build".into(), status: CheckRunStatus::Success, duration_secs: Some(105.0) }, - CheckRun { name: "coverage".into(), status: CheckRunStatus::Skipped, duration_secs: None }, + CheckRun { + name: "lint".into(), + status: CheckRunStatus::Success, + duration_secs: Some(23.0), + }, + CheckRun { + name: "typecheck".into(), + status: CheckRunStatus::Success, + duration_secs: Some(72.0), + }, + CheckRun { + name: "unit-tests".into(), + status: CheckRunStatus::Success, + duration_secs: Some(154.0), + }, + CheckRun { + name: "integration-tests".into(), + status: CheckRunStatus::Failure, + duration_secs: Some(296.0), + }, + CheckRun { + name: "e2e / chrome".into(), + status: CheckRunStatus::Failure, + duration_secs: Some(182.0), + }, + CheckRun { + name: "build".into(), + status: CheckRunStatus::Success, + duration_secs: Some(105.0), + }, + CheckRun { + name: "coverage".into(), + status: CheckRunStatus::Skipped, + duration_secs: None, + }, ], }), - timings: Some(RunTimings { elapsed_secs: 2100.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-m3n4o5p6".into(), resources: None }), + timings: Some(RunTimings { + elapsed_secs: 2100.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-m3n4o5p6".into(), + resources: None, + }), question: None, created_at: ts("2026-03-03T16:45:00Z"), }, RunListItem { id: "run-7".into(), - repository: RepositoryReference { name: "infrastructure".into() }, + repository: RepositoryReference { + name: "infrastructure".into(), + }, title: "Terraform module for Redis cluster".into(), - workflow: WorkflowReference { slug: "implement".into() }, + workflow: WorkflowReference { + slug: "implement".into(), + }, status: BoardColumn::Review, pull_request: Some(RunPullRequest { number: 156, @@ -648,23 +785,53 @@ mod runs { deletions: Some(0), comments: Some(1), checks: vec![ - CheckRun { name: "lint".into(), status: CheckRunStatus::Success, duration_secs: Some(18.0) }, - CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(56.0) }, - CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Pending, duration_secs: None }, - CheckRun { name: "integration-tests".into(), status: CheckRunStatus::Queued, duration_secs: None }, - CheckRun { name: "build".into(), status: CheckRunStatus::Pending, duration_secs: None }, + CheckRun { + name: "lint".into(), + status: CheckRunStatus::Success, + duration_secs: Some(18.0), + }, + CheckRun { + name: "typecheck".into(), + status: CheckRunStatus::Success, + duration_secs: Some(56.0), + }, + CheckRun { + name: "unit-tests".into(), + status: CheckRunStatus::Pending, + duration_secs: None, + }, + CheckRun { + name: "integration-tests".into(), + status: CheckRunStatus::Queued, + duration_secs: None, + }, + CheckRun { + name: "build".into(), + status: CheckRunStatus::Pending, + duration_secs: None, + }, ], }), - timings: Some(RunTimings { elapsed_secs: 720.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-y5z6a7b8".into(), resources: None }), + timings: Some(RunTimings { + elapsed_secs: 720.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-y5z6a7b8".into(), + resources: None, + }), question: None, created_at: ts("2026-03-03T11:00:00Z"), }, RunListItem { id: "run-8".into(), - repository: RepositoryReference { name: "api-server".into() }, + repository: RepositoryReference { + name: "api-server".into(), + }, title: "Implement webhook retry logic".into(), - workflow: WorkflowReference { slug: "implement".into() }, + workflow: WorkflowReference { + slug: "implement".into(), + }, status: BoardColumn::Merge, pull_request: Some(RunPullRequest { number: 1249, @@ -672,30 +839,88 @@ mod runs { deletions: Some(45), comments: Some(7), checks: vec![ - CheckRun { name: "lint".into(), status: CheckRunStatus::Success, duration_secs: Some(21.0) }, - CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(68.0) }, - CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(192.0) }, - CheckRun { name: "integration-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(334.0) }, - CheckRun { name: "e2e / chrome".into(), status: CheckRunStatus::Success, duration_secs: Some(262.0) }, - CheckRun { name: "e2e / firefox".into(), status: CheckRunStatus::Success, duration_secs: Some(285.0) }, - CheckRun { name: "build".into(), status: CheckRunStatus::Success, duration_secs: Some(121.0) }, - CheckRun { name: "deploy-preview".into(), status: CheckRunStatus::Success, duration_secs: Some(93.0) }, - CheckRun { name: "security-scan".into(), status: CheckRunStatus::Skipped, duration_secs: None }, - CheckRun { name: "performance".into(), status: CheckRunStatus::Success, duration_secs: Some(138.0) }, - CheckRun { name: "bundle-size".into(), status: CheckRunStatus::Success, duration_secs: Some(34.0) }, - CheckRun { name: "accessibility".into(), status: CheckRunStatus::Success, duration_secs: Some(72.0) }, + CheckRun { + name: "lint".into(), + status: CheckRunStatus::Success, + duration_secs: Some(21.0), + }, + CheckRun { + name: "typecheck".into(), + status: CheckRunStatus::Success, + duration_secs: Some(68.0), + }, + CheckRun { + name: "unit-tests".into(), + status: CheckRunStatus::Success, + duration_secs: Some(192.0), + }, + CheckRun { + name: "integration-tests".into(), + status: CheckRunStatus::Success, + duration_secs: Some(334.0), + }, + CheckRun { + name: "e2e / chrome".into(), + status: CheckRunStatus::Success, + duration_secs: Some(262.0), + }, + CheckRun { + name: "e2e / firefox".into(), + status: CheckRunStatus::Success, + duration_secs: Some(285.0), + }, + CheckRun { + name: "build".into(), + status: CheckRunStatus::Success, + duration_secs: Some(121.0), + }, + CheckRun { + name: "deploy-preview".into(), + status: CheckRunStatus::Success, + duration_secs: Some(93.0), + }, + CheckRun { + name: "security-scan".into(), + status: CheckRunStatus::Skipped, + duration_secs: None, + }, + CheckRun { + name: "performance".into(), + status: CheckRunStatus::Success, + duration_secs: Some(138.0), + }, + CheckRun { + name: "bundle-size".into(), + status: CheckRunStatus::Success, + duration_secs: Some(34.0), + }, + CheckRun { + name: "accessibility".into(), + status: CheckRunStatus::Success, + duration_secs: Some(72.0), + }, ], }), - timings: Some(RunTimings { elapsed_secs: 259200.0, elapsed_warning: Some(true) }), - sandbox: Some(RunSandbox { id: "sb-c9d0e1f2".into(), resources: None }), + timings: Some(RunTimings { + elapsed_secs: 259200.0, + elapsed_warning: Some(true), + }), + sandbox: Some(RunSandbox { + id: "sb-c9d0e1f2".into(), + resources: None, + }), question: None, created_at: ts("2026-02-28T14:00:00Z"), }, RunListItem { id: "run-9".into(), - repository: RepositoryReference { name: "cli-tools".into() }, + repository: RepositoryReference { + name: "cli-tools".into(), + }, title: "Add --verbose flag to run command".into(), - workflow: WorkflowReference { slug: "expand".into() }, + workflow: WorkflowReference { + slug: "expand".into(), + }, status: BoardColumn::Merge, pull_request: Some(RunPullRequest { number: 430, @@ -703,24 +928,58 @@ mod runs { deletions: Some(12), comments: Some(2), checks: vec![ - CheckRun { name: "lint".into(), status: CheckRunStatus::Success, duration_secs: Some(15.0) }, - CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(48.0) }, - CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(116.0) }, - CheckRun { name: "build".into(), status: CheckRunStatus::Success, duration_secs: Some(82.0) }, - CheckRun { name: "coverage".into(), status: CheckRunStatus::Success, duration_secs: Some(124.0) }, - CheckRun { name: "bundle-size".into(), status: CheckRunStatus::Skipped, duration_secs: None }, + CheckRun { + name: "lint".into(), + status: CheckRunStatus::Success, + duration_secs: Some(15.0), + }, + CheckRun { + name: "typecheck".into(), + status: CheckRunStatus::Success, + duration_secs: Some(48.0), + }, + CheckRun { + name: "unit-tests".into(), + status: CheckRunStatus::Success, + duration_secs: Some(116.0), + }, + CheckRun { + name: "build".into(), + status: CheckRunStatus::Success, + duration_secs: Some(82.0), + }, + CheckRun { + name: "coverage".into(), + status: CheckRunStatus::Success, + duration_secs: Some(124.0), + }, + CheckRun { + name: "bundle-size".into(), + status: CheckRunStatus::Skipped, + duration_secs: None, + }, ], }), - timings: Some(RunTimings { elapsed_secs: 3900.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-g3h4i5j6".into(), resources: None }), + timings: Some(RunTimings { + elapsed_secs: 3900.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-g3h4i5j6".into(), + resources: None, + }), question: None, created_at: ts("2026-02-27T09:00:00Z"), }, RunListItem { id: "run-10".into(), - repository: RepositoryReference { name: "shared-types".into() }, + repository: RepositoryReference { + name: "shared-types".into(), + }, title: "Export utility type helpers".into(), - workflow: WorkflowReference { slug: "sync_drift".into() }, + workflow: WorkflowReference { + slug: "sync_drift".into(), + }, status: BoardColumn::Merge, pull_request: Some(RunPullRequest { number: 76, @@ -728,14 +987,36 @@ mod runs { deletions: Some(8), comments: Some(0), checks: vec![ - CheckRun { name: "lint".into(), status: CheckRunStatus::Success, duration_secs: Some(12.0) }, - CheckRun { name: "typecheck".into(), status: CheckRunStatus::Success, duration_secs: Some(34.0) }, - CheckRun { name: "unit-tests".into(), status: CheckRunStatus::Success, duration_secs: Some(75.0) }, - CheckRun { name: "build".into(), status: CheckRunStatus::Success, duration_secs: Some(58.0) }, + CheckRun { + name: "lint".into(), + status: CheckRunStatus::Success, + duration_secs: Some(12.0), + }, + CheckRun { + name: "typecheck".into(), + status: CheckRunStatus::Success, + duration_secs: Some(34.0), + }, + CheckRun { + name: "unit-tests".into(), + status: CheckRunStatus::Success, + duration_secs: Some(75.0), + }, + CheckRun { + name: "build".into(), + status: CheckRunStatus::Success, + duration_secs: Some(58.0), + }, ], }), - timings: Some(RunTimings { elapsed_secs: 2880.0, elapsed_warning: Some(false) }), - sandbox: Some(RunSandbox { id: "sb-k7l8m9n0".into(), resources: None }), + timings: Some(RunTimings { + elapsed_secs: 2880.0, + elapsed_warning: Some(false), + }), + sandbox: Some(RunSandbox { + id: "sb-k7l8m9n0".into(), + resources: None, + }), question: None, created_at: ts("2026-02-26T08:00:00Z"), }, @@ -820,27 +1101,63 @@ mod runs { RunUsage { stages: vec![ UsageStage { - stage: UsageStageRef { id: "detect-drift".into(), name: "Detect Drift".into() }, - model: ModelReference { id: "Opus 4.6".into() }, - usage: TokenUsage { input_tokens: 12480, output_tokens: 3210, cost: 0.48 }, + stage: UsageStageRef { + id: "detect-drift".into(), + name: "Detect Drift".into(), + }, + model: ModelReference { + id: "Opus 4.6".into(), + }, + usage: TokenUsage { + input_tokens: 12480, + output_tokens: 3210, + cost: 0.48, + }, runtime_secs: 72.0, }, UsageStage { - stage: UsageStageRef { id: "propose-changes".into(), name: "Propose Changes".into() }, - model: ModelReference { id: "Gemini 3.1".into() }, - usage: TokenUsage { input_tokens: 28640, output_tokens: 8750, cost: 0.72 }, + stage: UsageStageRef { + id: "propose-changes".into(), + name: "Propose Changes".into(), + }, + model: ModelReference { + id: "Gemini 3.1".into(), + }, + usage: TokenUsage { + input_tokens: 28640, + output_tokens: 8750, + cost: 0.72, + }, runtime_secs: 154.0, }, UsageStage { - stage: UsageStageRef { id: "review-changes".into(), name: "Review Changes".into() }, - model: ModelReference { id: "Codex 5.3".into() }, - usage: TokenUsage { input_tokens: 9120, output_tokens: 2640, cost: 0.19 }, + stage: UsageStageRef { + id: "review-changes".into(), + name: "Review Changes".into(), + }, + model: ModelReference { + id: "Codex 5.3".into(), + }, + usage: TokenUsage { + input_tokens: 9120, + output_tokens: 2640, + cost: 0.19, + }, runtime_secs: 45.0, }, UsageStage { - stage: UsageStageRef { id: "apply-changes".into(), name: "Apply Changes".into() }, - model: ModelReference { id: "Opus 4.6".into() }, - usage: TokenUsage { input_tokens: 21300, output_tokens: 6480, cost: 0.87 }, + stage: UsageStageRef { + id: "apply-changes".into(), + name: "Apply Changes".into(), + }, + model: ModelReference { + id: "Opus 4.6".into(), + }, + usage: TokenUsage { + input_tokens: 21300, + output_tokens: 6480, + cost: 0.87, + }, runtime_secs: 118.0, }, ], @@ -852,19 +1169,37 @@ mod runs { }, by_model: vec![ UsageByModel { - model: ModelReference { id: "Opus 4.6".into() }, + model: ModelReference { + id: "Opus 4.6".into(), + }, stages: 2, - usage: TokenUsage { input_tokens: 33780, output_tokens: 9690, cost: 1.35 }, + usage: TokenUsage { + input_tokens: 33780, + output_tokens: 9690, + cost: 1.35, + }, }, UsageByModel { - model: ModelReference { id: "Gemini 3.1".into() }, + model: ModelReference { + id: "Gemini 3.1".into(), + }, stages: 1, - usage: TokenUsage { input_tokens: 28640, output_tokens: 8750, cost: 0.72 }, + usage: TokenUsage { + input_tokens: 28640, + output_tokens: 8750, + cost: 0.72, + }, }, UsageByModel { - model: ModelReference { id: "Codex 5.3".into() }, + model: ModelReference { + id: "Codex 5.3".into(), + }, stages: 1, - usage: TokenUsage { input_tokens: 9120, output_tokens: 2640, cost: 0.19 }, + usage: TokenUsage { + input_tokens: 9120, + output_tokens: 2640, + cost: 0.19, + }, }, ], } @@ -881,8 +1216,14 @@ mod runs { text: "Should we proceed with the proposed changes?".into(), question_type: QuestionType::YesNo, options: vec![ - ApiQuestionOption { key: "yes".into(), label: "Yes".into() }, - ApiQuestionOption { key: "no".into(), label: "No".into() }, + ApiQuestionOption { + key: "yes".into(), + label: "Yes".into(), + }, + ApiQuestionOption { + key: "no".into(), + label: "No".into(), + }, ], allow_freeform: false, }, @@ -891,8 +1232,14 @@ mod runs { text: "Which approach do you prefer for the migration?".into(), question_type: QuestionType::MultipleChoice, options: vec![ - ApiQuestionOption { key: "incremental".into(), label: "Incremental migration".into() }, - ApiQuestionOption { key: "big_bang".into(), label: "Big-bang rewrite".into() }, + ApiQuestionOption { + key: "incremental".into(), + label: "Incremental migration".into(), + }, + ApiQuestionOption { + key: "big_bang".into(), + label: "Big-bang rewrite".into(), + }, ], allow_freeform: true, }, @@ -919,9 +1266,10 @@ mod runs { preserve: None, daytona: Some(arc_workflows::daytona_sandbox::DaytonaConfig { auto_stop_interval: Some(60), - labels: Some(std::collections::HashMap::from([ - ("project".into(), "api-server".into()), - ])), + labels: Some(std::collections::HashMap::from([( + "project".into(), + "api-server".into(), + )])), snapshot: Some(arc_workflows::daytona_sandbox::DaytonaSnapshotConfig { name: "api-server-dev".into(), cpu: Some(4), @@ -935,7 +1283,10 @@ mod runs { env: None, }), vars: Some(std::collections::HashMap::from([ - ("repo_url".into(), "https://github.com/org/api-server".into()), + ( + "repo_url".into(), + "https://github.com/org/api-server".into(), + ), ("branch".into(), "feature/rate-limiting".into()), ])), hooks: vec![], @@ -959,19 +1310,37 @@ mod usage { }, by_model: vec![ UsageByModel { - model: ModelReference { id: "Opus 4.6".into() }, + model: ModelReference { + id: "Opus 4.6".into(), + }, stages: 18, - usage: TokenUsage { input_tokens: 304_020, output_tokens: 87_210, cost: 12.15 }, + usage: TokenUsage { + input_tokens: 304_020, + output_tokens: 87_210, + cost: 12.15, + }, }, UsageByModel { - model: ModelReference { id: "Gemini 3.1".into() }, + model: ModelReference { + id: "Gemini 3.1".into(), + }, stages: 9, - usage: TokenUsage { input_tokens: 257_760, output_tokens: 78_750, cost: 6.48 }, + usage: TokenUsage { + input_tokens: 257_760, + output_tokens: 78_750, + cost: 6.48, + }, }, UsageByModel { - model: ModelReference { id: "Codex 5.3".into() }, + model: ModelReference { + id: "Codex 5.3".into(), + }, stages: 9, - usage: TokenUsage { input_tokens: 82_080, output_tokens: 23_760, cost: 1.71 }, + usage: TokenUsage { + input_tokens: 82_080, + output_tokens: 23_760, + cost: 1.71, + }, }, ], } @@ -988,34 +1357,44 @@ mod workflows { name: "Fix Build".into(), slug: "fix_build".into(), filename: "fix_build.dot".into(), - last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-15T12:00:00Z") }), + last_run: Some(WorkflowLastRun { + ran_at: ts("2025-09-15T12:00:00Z"), + }), schedule: None, }, WorkflowListItem { name: "Implement Feature".into(), slug: "implement".into(), filename: "implement.dot".into(), - last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-11T10:00:00Z") }), + last_run: Some(WorkflowLastRun { + ran_at: ts("2025-09-11T10:00:00Z"), + }), schedule: None, }, WorkflowListItem { name: "Sync Drift".into(), slug: "sync_drift".into(), filename: "sync_drift.dot".into(), - last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-14T14:00:00Z") }), + last_run: Some(WorkflowLastRun { + ran_at: ts("2025-09-14T14:00:00Z"), + }), schedule: None, }, WorkflowListItem { name: "Expand Product".into(), slug: "expand".into(), filename: "expand.dot".into(), - last_run: Some(WorkflowLastRun { ran_at: ts("2025-09-01T08:00:00Z") }), + last_run: Some(WorkflowLastRun { + ran_at: ts("2025-09-01T08:00:00Z"), + }), schedule: None, }, ] } - fn run_config_to_api(cfg: arc_workflows::cli::run_config::WorkflowRunConfig) -> RunConfiguration { + fn run_config_to_api( + cfg: arc_workflows::cli::run_config::WorkflowRunConfig, + ) -> RunConfiguration { serde_json::from_value(serde_json::to_value(cfg).unwrap()).unwrap() } @@ -1952,8 +2331,13 @@ mod verifications { fn recent_results_from_def(defs: &[RecentResultDef]) -> Vec { defs.iter() .map(|r| RecentControlResult { - run: RunReference { id: r.run_id.into(), title: r.run_title.into() }, - workflow: WorkflowReference { slug: r.workflow.into() }, + run: RunReference { + id: r.run_id.into(), + title: r.run_title.into(), + }, + workflow: WorkflowReference { + slug: r.workflow.into(), + }, result: r.result, timestamp: ts(r.timestamp), }) @@ -2043,16 +2427,20 @@ mod verifications { ALL_CATEGORIES .iter() .flat_map(|cat| { - cat.controls.iter().map(move |c| VerificationControlListItem { - name: c.name.into(), - slug: c.slug.into(), - description: c.description.into(), - type_: c.type_, - mode: Some(c.mode), - f1: c.f1, - pass_at_1: c.pass_at_1, - criterion: CriterionReference { name: cat.name.into() }, - }) + cat.controls + .iter() + .map(move |c| VerificationControlListItem { + name: c.name.into(), + slug: c.slug.into(), + description: c.description.into(), + type_: c.type_, + mode: Some(c.mode), + f1: c.f1, + pass_at_1: c.pass_at_1, + criterion: CriterionReference { + name: cat.name.into(), + }, + }) }) .collect() } @@ -2085,7 +2473,9 @@ mod verifications { slug: ctrl.slug.into(), description: ctrl.description.into(), type_: Some(ctrl.type_), - criterion: CriterionReference { name: cat.name.into() }, + criterion: CriterionReference { + name: cat.name.into(), + }, }, performance: ControlPerformance { mode: ctrl.mode, @@ -2135,7 +2525,17 @@ mod retros { use super::ts; use arc_types::*; - fn stage(id: &str, label: &str, status: &str, duration_ms: i64, retries: i64, cost: Option, notes: Option<&str>, failure_reason: Option<&str>, files_touched: Vec<&str>) -> StageRetro { + fn stage( + id: &str, + label: &str, + status: &str, + duration_ms: i64, + retries: i64, + cost: Option, + notes: Option<&str>, + failure_reason: Option<&str>, + files_touched: Vec<&str>, + ) -> StageRetro { StageRetro { stage_id: id.into(), stage_label: label.into(), @@ -2324,8 +2724,13 @@ mod retros { pub fn list_items() -> Vec { vec![ RetroListItem { - run: RunReference { id: "run-1".into(), title: "Add rate limiting to auth endpoints".into() }, - workflow: WorkflowReference { slug: "implement".into() }, + run: RunReference { + id: "run-1".into(), + title: "Add rate limiting to auth endpoints".into(), + }, + workflow: WorkflowReference { + slug: "implement".into(), + }, timestamp: ts("2026-02-28T14:32:00Z"), smoothness: Some(SmoothnessRating::Smooth), stats: RetroStats { @@ -2344,8 +2749,13 @@ mod retros { friction_point_count: 0, }, RetroListItem { - run: RunReference { id: "run-2".into(), title: "Migrate to React Router v7".into() }, - workflow: WorkflowReference { slug: "implement".into() }, + run: RunReference { + id: "run-2".into(), + title: "Migrate to React Router v7".into(), + }, + workflow: WorkflowReference { + slug: "implement".into(), + }, timestamp: ts("2026-02-28T10:15:00Z"), smoothness: Some(SmoothnessRating::Bumpy), stats: RetroStats { @@ -2367,8 +2777,13 @@ mod retros { friction_point_count: 2, }, RetroListItem { - run: RunReference { id: "run-6".into(), title: "Add dark mode toggle".into() }, - workflow: WorkflowReference { slug: "implement".into() }, + run: RunReference { + id: "run-6".into(), + title: "Add dark mode toggle".into(), + }, + workflow: WorkflowReference { + slug: "implement".into(), + }, timestamp: ts("2026-02-27T16:45:00Z"), smoothness: Some(SmoothnessRating::Effortless), stats: RetroStats { @@ -2386,8 +2801,13 @@ mod retros { friction_point_count: 0, }, RetroListItem { - run: RunReference { id: "run-3".into(), title: "Fix config parsing for nested values".into() }, - workflow: WorkflowReference { slug: "fix_build".into() }, + run: RunReference { + id: "run-3".into(), + title: "Fix config parsing for nested values".into(), + }, + workflow: WorkflowReference { + slug: "fix_build".into(), + }, timestamp: ts("2026-02-27T09:20:00Z"), smoothness: Some(SmoothnessRating::Struggled), stats: RetroStats { @@ -2406,8 +2826,13 @@ mod retros { friction_point_count: 3, }, RetroListItem { - run: RunReference { id: "run-8".into(), title: "Implement webhook retry logic".into() }, - workflow: WorkflowReference { slug: "implement".into() }, + run: RunReference { + id: "run-8".into(), + title: "Implement webhook retry logic".into(), + }, + workflow: WorkflowReference { + slug: "implement".into(), + }, timestamp: ts("2026-02-26T11:00:00Z"), smoothness: Some(SmoothnessRating::Smooth), stats: RetroStats { @@ -2659,16 +3084,13 @@ mod settings { network: Some(arc_workflows::daytona_sandbox::DaytonaNetwork::Block), }), exe: None, - env: None, + env: None, }), vars: None, checkpoint: Default::default(), }, - hook_config: arc_workflows::hook::HookConfig { - hooks: vec![], - }, + hook_config: arc_workflows::hook::HookConfig { hooks: vec![] }, }) .unwrap() } } - diff --git a/crates/arc-api/src/serve.rs b/crates/arc-api/src/serve.rs index e821b6b4a..698ffd4c4 100644 --- a/crates/arc-api/src/serve.rs +++ b/crates/arc-api/src/serve.rs @@ -120,10 +120,8 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: let (auth_mode, client_auth, max_concurrent_runs) = { let cfg = shared_config.read().expect("config lock poisoned"); - let auth_mode = crate::jwt_auth::resolve_auth_mode( - &cfg.api, - cfg.web.auth.allowed_usernames.clone(), - ); + let auth_mode = + crate::jwt_auth::resolve_auth_mode(&cfg.api, cfg.web.auth.allowed_usernames.clone()); let client_auth = cfg .api .tls @@ -143,7 +141,13 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: cfg.git.author.email.clone(), ) }; - let state = crate::server::create_app_state_with_options(db, factory, dry_run_mode, max_concurrent_runs, git_author); + let state = crate::server::create_app_state_with_options( + db, + factory, + dry_run_mode, + max_concurrent_runs, + git_author, + ); crate::server::spawn_scheduler(Arc::clone(&state)); let router = build_router(state, auth_mode); diff --git a/crates/arc-api/src/server.rs b/crates/arc-api/src/server.rs index 3ec76f9fd..6b860bdc8 100644 --- a/crates/arc-api/src/server.rs +++ b/crates/arc-api/src/server.rs @@ -137,11 +137,7 @@ pub fn build_router(state: Arc, auth_mode: AuthMode) -> Router { let demo = demo_router.clone(); let real = real_router.clone(); async move { - if req - .headers() - .get("x-arc-demo") - .map_or(false, |v| v == "1") - { + if req.headers().get("x-arc-demo").map_or(false, |v| v == "1") { demo.oneshot(req).await } else { real.oneshot(req).await @@ -267,26 +263,34 @@ fn real_routes() -> Router> { .route("/runs/{id}/preview", post(not_implemented)) .route("/workflows", get(not_implemented)) .route("/workflows/{name}", get(not_implemented)) - .route( - "/workflows/{name}/runs", - get(not_implemented), - ) + .route("/workflows/{name}/runs", get(not_implemented)) .route("/verification/criteria", get(not_implemented)) .route("/verification/criteria/{id}", get(not_implemented)) .route("/verification/controls", get(not_implemented)) .route("/verification/controls/{id}", get(not_implemented)) .route("/retros", get(not_implemented)) - .route("/sessions", get(crate::sessions::list_sessions).post(crate::sessions::create_session)) + .route( + "/sessions", + get(crate::sessions::list_sessions).post(crate::sessions::create_session), + ) .route("/sessions/{id}", get(crate::sessions::retrieve_session)) - .route("/sessions/{id}/messages", post(crate::sessions::send_message)) - .route("/sessions/{id}/events", get(crate::sessions::stream_session_events)) + .route( + "/sessions/{id}/messages", + post(crate::sessions::send_message), + ) + .route( + "/sessions/{id}/events", + get(crate::sessions::stream_session_events), + ) .route( "/insights/queries", get(not_implemented).post(not_implemented), ) .route( "/insights/queries/{id}", - get(not_implemented).put(not_implemented).delete(not_implemented), + get(not_implemented) + .put(not_implemented) + .delete(not_implemented), ) .route("/insights/execute", post(not_implemented)) .route("/insights/history", get(not_implemented)) @@ -318,7 +322,8 @@ async fn health() -> Response { async fn openapi_spec() -> Response { let yaml = include_str!("../../../docs/api-reference/arc-api.yaml"); - let value: serde_json::Value = serde_yaml::from_str(yaml).expect("embedded OpenAPI YAML is invalid"); + let value: serde_json::Value = + serde_yaml::from_str(yaml).expect("embedded OpenAPI YAML is invalid"); Json(value).into_response() } @@ -330,7 +335,10 @@ async fn get_aggregate_usage( _auth: AuthenticatedService, State(state): State>, ) -> Response { - let agg = state.aggregate_usage.lock().expect("aggregate_usage lock poisoned"); + let agg = state + .aggregate_usage + .lock() + .expect("aggregate_usage lock poisoned"); let by_model: Vec = agg .by_model .iter() @@ -365,7 +373,13 @@ pub fn create_app_state( db: sqlx::SqlitePool, registry_factory: impl Fn(Arc) -> HandlerRegistry + Send + Sync + 'static, ) -> Arc { - create_app_state_with_options(db, registry_factory, false, 5, arc_workflows::git::GitAuthor::default()) + create_app_state_with_options( + db, + registry_factory, + false, + 5, + arc_workflows::git::GitAuthor::default(), + ) } /// Create an `AppState` with the given database pool, registry factory, dry-run flag, and concurrency limit. @@ -404,7 +418,9 @@ async fn list_runs( .map(|(id, managed_run)| RunStatusResponse { id: id.clone(), status: managed_run.status, - error: managed_run.error.as_ref().map(|msg| arc_types::RunError { message: msg.clone() }), + error: managed_run.error.as_ref().map(|msg| arc_types::RunError { + message: msg.clone(), + }), queue_position: queue_positions.get(id).copied(), created_at: managed_run.created_at, }) @@ -485,7 +501,6 @@ async fn start_run( queue_position: None, created_at, }), - ) .into_response() } @@ -680,7 +695,9 @@ pub fn spawn_scheduler(state: Arc) { let runs = state.runs.lock().expect("runs lock poisoned"); let active = runs .values() - .filter(|r| r.status == RunStatus::Starting || r.status == RunStatus::Running) + .filter(|r| { + r.status == RunStatus::Starting || r.status == RunStatus::Running + }) .count(); if active >= state.max_concurrent_runs { break; @@ -721,7 +738,9 @@ async fn get_run_status( Json(RunStatusResponse { id: id.clone(), status: managed_run.status, - error: managed_run.error.as_ref().map(|msg| arc_types::RunError { message: msg.clone() }), + error: managed_run.error.as_ref().map(|msg| arc_types::RunError { + message: msg.clone(), + }), created_at: managed_run.created_at, queue_position, }), @@ -742,7 +761,13 @@ async fn get_questions( Some(managed_run) => { let interviewer = match &managed_run.interviewer { Some(i) => i, - None => return (StatusCode::OK, Json(ListResponse::new(Vec::::new()))).into_response(), + None => { + return ( + StatusCode::OK, + Json(ListResponse::new(Vec::::new())), + ) + .into_response() + } }; let pending = interviewer.pending_questions(); let questions: Vec = pending @@ -809,9 +834,8 @@ async fn submit_answer( let pq = pending.iter().find(|pq| pq.id == qid); let mut options = Vec::new(); for key in &req.selected_option_keys { - let opt = pq.and_then(|pq| { - pq.question.options.iter().find(|o| o.key == *key).cloned() - }); + let opt = pq + .and_then(|pq| pq.question.options.iter().find(|o| o.key == *key).cloned()); match opt { Some(o) => options.push(o), None => { @@ -853,7 +877,9 @@ async fn get_events( match runs.get(&id) { Some(managed_run) => match &managed_run.event_tx { Some(tx) => tx.subscribe(), - None => return ApiError::new(StatusCode::GONE, "Event stream closed.").into_response(), + None => { + return ApiError::new(StatusCode::GONE, "Event stream closed.").into_response() + } }, None => return ApiError::not_found("Run not found.").into_response(), } @@ -1033,9 +1059,8 @@ async fn create_completion( if use_stream { let sse_stream = futures_util::stream::iter(vec![ Ok::<_, std::convert::Infallible>( - Event::default() - .event("message_start") - .data(serde_json::json!({ + Event::default().event("message_start").data( + serde_json::json!({ "type": "message_start", "message": { "id": msg_id, @@ -1046,7 +1071,9 @@ async fn create_completion( "stop_reason": null, "usage": {"input_tokens": 0} } - }).to_string()), + }) + .to_string(), + ), ), Ok(Event::default() .event("message_stop") @@ -1081,10 +1108,12 @@ async fn create_completion( let msg_id = ulid::Ulid::new().to_string(); let model_for_stream = model_id.clone(); - let sse_stream = futures_util::stream::once(futures_util::future::ready(Ok::<_, std::convert::Infallible>( - Event::default() - .event("message_start") - .data(serde_json::json!({ + let sse_stream = futures_util::stream::once(futures_util::future::ready(Ok::< + _, + std::convert::Infallible, + >( + Event::default().event("message_start").data( + serde_json::json!({ "type": "message_start", "message": { "id": msg_id, @@ -1095,69 +1124,74 @@ async fn create_completion( "stop_reason": null, "usage": {"input_tokens": 0} } - }).to_string()), + }) + .to_string(), + ), ))) .chain(futures_util::stream::once(futures_util::future::ready(Ok( - Event::default() - .event("content_block_start") - .data(serde_json::json!({ + Event::default().event("content_block_start").data( + serde_json::json!({ "type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""} - }).to_string()), + }) + .to_string(), + ), )))) .chain(futures_util::stream::once(futures_util::future::ready(Ok( Event::default() .event("ping") .data(serde_json::json!({"type": "ping"}).to_string()), )))) - .chain( - tokio_stream::StreamExt::map(stream_result, |event| { - match event { - Ok(arc_llm::types::StreamEvent::TextDelta { delta, .. }) => Ok( - Event::default() - .event("content_block_delta") - .data(serde_json::json!({ - "type": "content_block_delta", - "index": 0, - "delta": {"type": "text_delta", "text": delta} - }).to_string()), - ), - Ok(arc_llm::types::StreamEvent::TextEnd { .. }) => Ok( - Event::default() - .event("content_block_stop") - .data(serde_json::json!({"type": "content_block_stop", "index": 0}).to_string()), - ), - Ok(arc_llm::types::StreamEvent::Finish { finish_reason, usage, .. }) => Ok( - Event::default() - .event("message_delta") - .data(serde_json::json!({ - "type": "message_delta", - "delta": {"stop_reason": finish_reason_to_stop_reason(&finish_reason)}, - "usage": {"output_tokens": usage.output_tokens} - }).to_string()), - ), - Ok(arc_llm::types::StreamEvent::Error { error, .. }) => Ok( - Event::default() - .event("error") - .data(serde_json::json!({ - "type": "error", - "error": {"type": "server_error", "message": error.to_string()} - }).to_string()), - ), - Err(e) => Ok( - Event::default() - .event("error") - .data(serde_json::json!({ - "type": "error", - "error": {"type": "server_error", "message": e.to_string()} - }).to_string()), - ), - // Skip events we don't map (StreamStart, TextStart, reasoning, tool calls, etc.) - _ => Ok(Event::default().comment("ignored")), + .chain(tokio_stream::StreamExt::map(stream_result, |event| { + match event { + Ok(arc_llm::types::StreamEvent::TextDelta { delta, .. }) => { + Ok(Event::default().event("content_block_delta").data( + serde_json::json!({ + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": delta} + }) + .to_string(), + )) } - }), - ) + Ok(arc_llm::types::StreamEvent::TextEnd { .. }) => { + Ok(Event::default().event("content_block_stop").data( + serde_json::json!({"type": "content_block_stop", "index": 0}).to_string(), + )) + } + Ok(arc_llm::types::StreamEvent::Finish { + finish_reason, + usage, + .. + }) => Ok(Event::default().event("message_delta").data( + serde_json::json!({ + "type": "message_delta", + "delta": {"stop_reason": finish_reason_to_stop_reason(&finish_reason)}, + "usage": {"output_tokens": usage.output_tokens} + }) + .to_string(), + )), + Ok(arc_llm::types::StreamEvent::Error { error, .. }) => { + Ok(Event::default().event("error").data( + serde_json::json!({ + "type": "error", + "error": {"type": "server_error", "message": error.to_string()} + }) + .to_string(), + )) + } + Err(e) => Ok(Event::default().event("error").data( + serde_json::json!({ + "type": "error", + "error": {"type": "server_error", "message": e.to_string()} + }) + .to_string(), + )), + // Skip events we don't map (StreamStart, TextStart, reasoning, tool calls, etc.) + _ => Ok(Event::default().comment("ignored")), + } + })) .chain(futures_util::stream::once(futures_util::future::ready(Ok( Event::default() .event("message_stop") @@ -1193,10 +1227,8 @@ async fn create_completion( output: result.output, }) .into_response(), - Err(e) => { - ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) - .into_response() - } + Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) + .into_response(), } } else { match arc_llm::generate::generate(params).await { @@ -1212,10 +1244,8 @@ async fn create_completion( output: None, }) .into_response(), - Err(e) => { - ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) - .into_response() - } + Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("LLM error: {e}")) + .into_response(), } } } @@ -1266,7 +1296,11 @@ async fn get_graph( { Ok(child) => child, Err(_) => { - return ApiError::new(StatusCode::BAD_GATEWAY, "Graphviz dot command not available.").into_response(); + return ApiError::new( + StatusCode::BAD_GATEWAY, + "Graphviz dot command not available.", + ) + .into_response(); } }; @@ -1287,9 +1321,8 @@ async fn get_graph( let stderr = String::from_utf8_lossy(&output.stderr); ApiError::new(StatusCode::BAD_GATEWAY, format!("dot failed: {stderr}")).into_response() } - Err(e) => { - ApiError::new(StatusCode::BAD_GATEWAY, format!("dot process error: {e}")).into_response() - } + Err(e) => ApiError::new(StatusCode::BAD_GATEWAY, format!("dot process error: {e}")) + .into_response(), } } @@ -1376,7 +1409,13 @@ mod tests { #[tokio::test] async fn test_model_dry_run_returns_ok() { - let state = create_app_state_with_options(test_db().await, test_registry, true, 5, arc_workflows::git::GitAuthor::default()); + let state = create_app_state_with_options( + test_db().await, + test_registry, + true, + 5, + arc_workflows::git::GitAuthor::default(), + ); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -1396,7 +1435,13 @@ mod tests { #[tokio::test] async fn test_model_dry_run_unknown_returns_404() { - let state = create_app_state_with_options(test_db().await, test_registry, true, 5, arc_workflows::git::GitAuthor::default()); + let state = create_app_state_with_options( + test_db().await, + test_registry, + true, + 5, + arc_workflows::git::GitAuthor::default(), + ); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -1484,7 +1529,10 @@ mod tests { assert_eq!(body["id"].as_str().unwrap(), run_id); let status = body["status"].as_str().unwrap(); assert!( - status == "queued" || status == "starting" || status == "running" || status == "completed", + status == "queued" + || status == "starting" + || status == "running" + || status == "completed", "unexpected status: {status}" ); } @@ -2077,7 +2125,13 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn concurrency_limit_respected() { - let state = create_app_state_with_options(test_db().await, test_registry, false, 1, arc_workflows::git::GitAuthor::default()); + let state = create_app_state_with_options( + test_db().await, + test_registry, + false, + 1, + arc_workflows::git::GitAuthor::default(), + ); let app = test_app_with_scheduler(state); // Submit two runs with max_concurrent_runs=1 diff --git a/crates/arc-api/src/sessions.rs b/crates/arc-api/src/sessions.rs index 72632b53a..dce84e8bf 100644 --- a/crates/arc-api/src/sessions.rs +++ b/crates/arc-api/src/sessions.rs @@ -34,10 +34,17 @@ pub struct SessionState { #[derive(Clone, Debug)] pub enum SessionEvent { - TextDelta { delta: String }, - AssistantTurnComplete { content: String, created_at: chrono::DateTime }, + TextDelta { + delta: String, + }, + AssistantTurnComplete { + content: String, + created_at: chrono::DateTime, + }, Done, - Error { message: String }, + Error { + message: String, + }, } fn generate_title(content: &str) -> String { @@ -77,12 +84,7 @@ fn turns_to_messages(turns: &[arc_types::SessionTurn]) -> Vec { let now = chrono::Utc::now(); - session.turns.push(arc_types::SessionTurn::UserTurn( - arc_types::UserTurn { + session + .turns + .push(arc_types::SessionTurn::UserTurn(arc_types::UserTurn { kind: arc_types::UserTurnKind::User, content: req.content, created_at: now, - }, - )); + })); session.updated_at = now; let seq = session.generation_seq.fetch_add(1, Ordering::Relaxed) + 1; seq @@ -304,12 +301,7 @@ pub async fn send_message( } }; - spawn_generation( - Arc::clone(&state.sessions), - id, - state.dry_run, - seq, - ); + spawn_generation(Arc::clone(&state.sessions), id, state.dry_run, seq); ( StatusCode::ACCEPTED, @@ -333,8 +325,8 @@ pub async fn stream_session_events( use tokio_stream::StreamExt; - let stream = tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(|result| { - match result { + let stream = + tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(|result| match result { Ok(event) => { let sse: Option = match event { SessionEvent::TextDelta { delta } => Some( @@ -342,7 +334,10 @@ pub async fn stream_session_events( .event("content_delta") .data(serde_json::json!({"delta": delta}).to_string()), ), - SessionEvent::AssistantTurnComplete { content, created_at } => Some( + SessionEvent::AssistantTurnComplete { + content, + created_at, + } => Some( Event::default().event("assistant_turn").data( serde_json::json!({ "kind": "assistant", @@ -352,11 +347,7 @@ pub async fn stream_session_events( .to_string(), ), ), - SessionEvent::Done => Some( - Event::default() - .event("done") - .data("{}"), - ), + SessionEvent::Done => Some(Event::default().event("done").data("{}")), SessionEvent::Error { message } => Some( Event::default() .event("error") @@ -366,8 +357,7 @@ pub async fn stream_session_events( sse.map(|e| Ok::<_, std::convert::Infallible>(e)) } Err(_) => None, - } - }); + }); Sse::new(stream).into_response() } diff --git a/crates/arc-api/tests/integration.rs b/crates/arc-api/tests/integration.rs index 26731c5a6..7760813cb 100644 --- a/crates/arc-api/tests/integration.rs +++ b/crates/arc-api/tests/integration.rs @@ -420,8 +420,8 @@ mod server_lifecycle { use arc_api::server::{build_router, create_app_state}; use arc_workflows::handler::agent::AgentHandler; use arc_workflows::handler::exit::ExitHandler; - use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::human::HumanHandler; + use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::HandlerRegistry; use arc_workflows::interviewer::Interviewer; use axum::body::Body; diff --git a/crates/arc-api/tests/pagination.rs b/crates/arc-api/tests/pagination.rs index 198566253..e33ae1816 100644 --- a/crates/arc-api/tests/pagination.rs +++ b/crates/arc-api/tests/pagination.rs @@ -43,10 +43,7 @@ async fn get_json(app: axum::Router, uri: &str) -> serde_json::Value { /// Assert that a value has the paginated shape: `{ data: [...], meta: { has_more: bool } }` fn assert_paginated_shape(json: &serde_json::Value, context: &str) { assert!(json.get("data").is_some(), "{context}: missing 'data' key"); - assert!( - json["data"].is_array(), - "{context}: 'data' is not an array" - ); + assert!(json["data"].is_array(), "{context}: 'data' is not an array"); assert!(json.get("meta").is_some(), "{context}: missing 'meta' key"); assert!( json["meta"].get("has_more").is_some(), diff --git a/crates/arc-cli/src/cli_config.rs b/crates/arc-cli/src/cli_config.rs index b256104bc..84fce8d5e 100644 --- a/crates/arc-cli/src/cli_config.rs +++ b/crates/arc-cli/src/cli_config.rs @@ -69,9 +69,7 @@ pub fn resolve_mode( cli_server_url: Option<&str>, config: &CliConfig, ) -> ResolvedMode { - let mode = cli_mode - .or_else(|| config.mode.clone()) - .unwrap_or_default(); + let mode = cli_mode.or_else(|| config.mode.clone()).unwrap_or_default(); let server_defaults = config.server.as_ref(); diff --git a/crates/arc-cli/src/main.rs b/crates/arc-cli/src/main.rs index eee20a228..7ef1d0660 100644 --- a/crates/arc-cli/src/main.rs +++ b/crates/arc-cli/src/main.rs @@ -34,7 +34,9 @@ fn parse_execution_mode(s: &str) -> Result { match s { "standalone" => Ok(cli_config::ExecutionMode::Standalone), "server" => Ok(cli_config::ExecutionMode::Server), - _ => Err(format!("invalid mode '{s}', expected 'standalone' or 'server'")), + _ => Err(format!( + "invalid mode '{s}', expected 'standalone' or 'server'" + )), } } @@ -138,15 +140,18 @@ async fn main() -> Result<()> { }; let config_log_level = if let Command::Serve(ref args) = cli.command { - let server_config = - arc_api::server_config::load_server_config(args.config.as_deref())?; + let server_config = arc_api::server_config::load_server_config(args.config.as_deref())?; server_config.log.level } else { let cli_config = cli_config::load_cli_config(None)?; cli_config.log.level }; - let log_prefix = if command_name == "serve" { "serve" } else { "cli" }; + let log_prefix = if command_name == "serve" { + "serve" + } else { + "cli" + }; if let Err(err) = logging::init_tracing(cli.debug, config_log_level.as_deref(), log_prefix) { eprintln!("Warning: failed to initialize logging: {err:#}"); } @@ -162,15 +167,11 @@ async fn main() -> Result<()> { if args.model.is_none() { args.model = llm_defaults.and_then(|l| l.model.clone()); } - let resolved = cli_config::resolve_mode( - cli.mode, - cli.server_url.as_deref(), - &cli_config, - ); + let resolved = + cli_config::resolve_mode(cli.mode, cli.server_url.as_deref(), &cli_config); match resolved.mode { cli_config::ExecutionMode::Server => { - let client = - cli_config::build_server_client(resolved.tls.as_ref())?; + let client = cli_config::build_server_client(resolved.tls.as_ref())?; let server = arc_llm::cli::ServerConnection { client, base_url: resolved.server_base_url, @@ -186,15 +187,11 @@ async fn main() -> Result<()> { if args.model.is_none() { args.model = llm_defaults.and_then(|l| l.model.clone()); } - let resolved = cli_config::resolve_mode( - cli.mode, - cli.server_url.as_deref(), - &cli_config, - ); + let resolved = + cli_config::resolve_mode(cli.mode, cli.server_url.as_deref(), &cli_config); match resolved.mode { cli_config::ExecutionMode::Server => { - let client = - cli_config::build_server_client(resolved.tls.as_ref())?; + let client = cli_config::build_server_client(resolved.tls.as_ref())?; let server = arc_llm::cli::ServerConnection { client, base_url: resolved.server_base_url, @@ -262,11 +259,8 @@ async fn main() -> Result<()> { } Command::Model { command } => { let cli_config = cli_config::load_cli_config(None)?; - let resolved = cli_config::resolve_mode( - cli.mode, - cli.server_url.as_deref(), - &cli_config, - ); + let resolved = + cli_config::resolve_mode(cli.mode, cli.server_url.as_deref(), &cli_config); let server = match resolved.mode { cli_config::ExecutionMode::Server => { let client = cli_config::build_server_client(resolved.tls.as_ref())?; diff --git a/crates/arc-exe/src/lib.rs b/crates/arc-exe/src/lib.rs index cf5e94099..3ce01cae7 100644 --- a/crates/arc-exe/src/lib.rs +++ b/crates/arc-exe/src/lib.rs @@ -59,7 +59,14 @@ pub struct ExeSandbox { /// Factory for creating data-plane SSH runners, used during initialize(). /// In production, this connects to the VM host via OpensshRunner. /// In tests, this is replaced with a closure that returns a MockSshRunner. - data_ssh_factory: Box std::pin::Pin, String>> + Send>> + Send + Sync>, + data_ssh_factory: Box< + dyn Fn( + &str, + ) -> std::pin::Pin< + Box, String>> + Send>, + > + Send + + Sync, + >, } impl ExeSandbox { @@ -121,27 +128,24 @@ impl Sandbox for ExeSandbox { let init_start = Instant::now(); // Create a new VM via the management plane - let output = self - .mgmt_ssh - .run_command("new --json") - .await - .map_err(|e| { - let err = format!("Failed to create exe.dev VM: {e}"); - let duration_ms = - u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(SandboxEvent::InitializeFailed { - provider: PROVIDER.into(), - error: err.clone(), - duration_ms, - }); - err - })?; + let output = self.mgmt_ssh.run_command("new --json").await.map_err(|e| { + let err = format!("Failed to create exe.dev VM: {e}"); + let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit(SandboxEvent::InitializeFailed { + provider: PROVIDER.into(), + error: err.clone(), + duration_ms, + }); + err + })?; if output.exit_code != 0 { let stderr = String::from_utf8_lossy(&output.stderr); - let err = format!("exe.dev VM creation failed (exit {}): {stderr}", output.exit_code); - let duration_ms = - u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + let err = format!( + "exe.dev VM creation failed (exit {}): {stderr}", + output.exit_code + ); + let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::InitializeFailed { provider: PROVIDER.into(), error: err.clone(), @@ -152,18 +156,16 @@ impl Sandbox for ExeSandbox { // Parse JSON response to get VM name and host let stdout = String::from_utf8_lossy(&output.stdout); - let json: serde_json::Value = serde_json::from_str(stdout.trim()) - .map_err(|e| { - let err = format!("Failed to parse exe.dev response: {e}"); - let duration_ms = - u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); - self.emit(SandboxEvent::InitializeFailed { - provider: PROVIDER.into(), - error: err.clone(), - duration_ms, - }); - err - })?; + let json: serde_json::Value = serde_json::from_str(stdout.trim()).map_err(|e| { + let err = format!("Failed to parse exe.dev response: {e}"); + let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit(SandboxEvent::InitializeFailed { + provider: PROVIDER.into(), + error: err.clone(), + duration_ms, + }); + err + })?; let vm_name = json["vm_name"] .as_str() @@ -184,8 +186,7 @@ impl Sandbox for ExeSandbox { // Create data-plane SSH connection let runner = (self.data_ssh_factory)(&data_host).await.map_err(|e| { let err = format!("Failed to connect to exe.dev VM {data_host}: {e}"); - let duration_ms = - u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::InitializeFailed { provider: PROVIDER.into(), error: err.clone(), @@ -310,8 +311,8 @@ impl Sandbox for ExeSandbox { return Err(format!("Failed to read {resolved}: {stderr}")); } - let content = - String::from_utf8(output.stdout).map_err(|e| format!("File is not valid UTF-8: {e}"))?; + let content = String::from_utf8(output.stdout) + .map_err(|e| format!("File is not valid UTF-8: {e}"))?; Ok(format_lines_numbered(&content, offset, limit)) } @@ -372,9 +373,7 @@ impl Sandbox for ExeSandbox { max_depth, ); - let result = self - .exec_command(&cmd, 30_000, None, None, None) - .await?; + let result = self.exec_command(&cmd, 30_000, None, None, None).await?; if result.exit_code != 0 { return Err(format!( @@ -541,10 +540,7 @@ impl Sandbox for ExeSandbox { } fn sandbox_info(&self) -> String { - self.vm_name - .get() - .cloned() - .unwrap_or_default() + self.vm_name.get().cloned().unwrap_or_default() } } @@ -817,7 +813,10 @@ mod tests { data.queue_response("a\nb\nc\nd\ne\n", "", 0); let sandbox = sandbox_with_mock_data(data); - let content = sandbox.read_file("test.txt", Some(1), Some(2)).await.unwrap(); + let content = sandbox + .read_file("test.txt", Some(1), Some(2)) + .await + .unwrap(); assert!(content.contains("2 | b")); assert!(content.contains("3 | c")); assert!(!content.contains("1 | a")); @@ -831,10 +830,7 @@ mod tests { data.queue_response("content\n", "", 0); let sandbox = sandbox_with_mock_data(data); - sandbox - .read_file("/etc/hosts", None, None) - .await - .unwrap(); + sandbox.read_file("/etc/hosts", None, None).await.unwrap(); let recorded = commands.lock().unwrap(); assert!( @@ -858,7 +854,10 @@ mod tests { data.queue_response("", "", 0); let sandbox = sandbox_with_mock_data(data); - sandbox.write_file("src/main.rs", "fn main() {}").await.unwrap(); + sandbox + .write_file("src/main.rs", "fn main() {}") + .await + .unwrap(); let recorded = uploads.lock().unwrap(); assert_eq!(recorded[0].path, "/home/exedev/src/main.rs"); @@ -936,7 +935,11 @@ mod tests { // find output for list_directory (run via exec_command, so two responses: // first for the rg_available check if it fires... but exec_command calls // run_command_with_timeout directly, which will get the next response) - data.queue_response("f\t1024\tfile.txt\nd\t4096\tsrc\nf\t512\tREADME.md\n", "", 0); + data.queue_response( + "f\t1024\tfile.txt\nd\t4096\tsrc\nf\t512\tREADME.md\n", + "", + 0, + ); let sandbox = sandbox_with_mock_data(data); let entries = sandbox.list_directory(".", None).await.unwrap(); @@ -959,7 +962,11 @@ mod tests { // First call: rg --version check (cached) data.queue_response("ripgrep 14.0.0", "", 0); // Second call: the actual grep - data.queue_response("src/main.rs:1:fn main() {}\nsrc/lib.rs:5:fn helper() {}\n", "", 0); + data.queue_response( + "src/main.rs:1:fn main() {}\nsrc/lib.rs:5:fn helper() {}\n", + "", + 0, + ); let sandbox = sandbox_with_mock_data(data); let results = sandbox diff --git a/crates/arc-llm/src/catalog.rs b/crates/arc-llm/src/catalog.rs index 3eeddd12a..00f698ca7 100644 --- a/crates/arc-llm/src/catalog.rs +++ b/crates/arc-llm/src/catalog.rs @@ -73,7 +73,9 @@ pub fn closest_model(target_provider: &str, reference: &ModelInfo) -> Option R .json(&body) .send() .await - .with_context(|| { - format!("Failed to connect to server at {}", server.base_url) - })?; + .with_context(|| format!("Failed to connect to server at {}", server.base_url))?; let status = response.status(); if !status.is_success() { @@ -654,9 +657,7 @@ pub async fn run_chat_via_server(args: ChatArgs, server: &ServerConnection) -> R .as_str() .context("Missing session id in response")? .to_string(); - let model_id = create_resp["model"]["id"] - .as_str() - .unwrap_or("unknown"); + let model_id = create_resp["model"]["id"].as_str().unwrap_or("unknown"); eprintln!("Using model: {model_id}"); // Stream events @@ -686,9 +687,7 @@ pub async fn run_chat_via_server(args: ChatArgs, server: &ServerConnection) -> R .json(&body) .send() .await - .with_context(|| { - format!("Failed to connect to server at {}", server.base_url) - })?; + .with_context(|| format!("Failed to connect to server at {}", server.base_url))?; let status = response.status(); if !status.is_success() { @@ -818,8 +817,7 @@ async fn test_models_via_server( let mut failures = 0u32; for info in &models_to_test { - let result = - test_model_via_server(&server.client, &server.base_url, &info.id).await; + let result = test_model_via_server(&server.client, &server.base_url, &info.id).await; let (status_color, status) = match result { Ok(resp) if resp.status == "ok" => (&s.green, "ok".to_string()), @@ -1160,15 +1158,20 @@ mod tests { #[tokio::test] async fn test_model_via_server_parses_ok() { let server = httpmock::MockServer::start_async().await; - server.mock_async(|when, then| { - when.method("POST").path("/models/test-model/test"); - then.status(200) - .header("Content-Type", "application/json") - .body(serde_json::json!({ - "model_id": "test-model", - "status": "ok" - }).to_string()); - }).await; + server + .mock_async(|when, then| { + when.method("POST").path("/models/test-model/test"); + then.status(200) + .header("Content-Type", "application/json") + .body( + serde_json::json!({ + "model_id": "test-model", + "status": "ok" + }) + .to_string(), + ); + }) + .await; let client = reqwest::Client::new(); let resp = test_model_via_server(&client, &server.url(""), "test-model") @@ -1182,16 +1185,21 @@ mod tests { #[tokio::test] async fn test_model_via_server_parses_error() { let server = httpmock::MockServer::start_async().await; - server.mock_async(|when, then| { - when.method("POST").path("/models/test-model/test"); - then.status(200) - .header("Content-Type", "application/json") - .body(serde_json::json!({ - "model_id": "test-model", - "status": "error", - "error_message": "timeout" - }).to_string()); - }).await; + server + .mock_async(|when, then| { + when.method("POST").path("/models/test-model/test"); + then.status(200) + .header("Content-Type", "application/json") + .body( + serde_json::json!({ + "model_id": "test-model", + "status": "error", + "error_message": "timeout" + }) + .to_string(), + ); + }) + .await; let client = reqwest::Client::new(); let resp = test_model_via_server(&client, &server.url(""), "test-model") @@ -1205,14 +1213,16 @@ mod tests { #[tokio::test] async fn test_model_via_server_404() { let server = httpmock::MockServer::start_async().await; - server.mock_async(|when, then| { - when.method("POST").path("/models/bad-model/test"); - then.status(404) + server + .mock_async(|when, then| { + when.method("POST").path("/models/bad-model/test"); + then.status(404) .header("Content-Type", "application/json") .body(serde_json::json!({ "errors": [{"status": "404", "title": "Not Found", "detail": "Model not found"}] }).to_string()); - }).await; + }) + .await; let client = reqwest::Client::new(); let result = test_model_via_server(&client, &server.url(""), "bad-model").await; @@ -1260,38 +1270,43 @@ mod tests { #[tokio::test] async fn fetch_models_from_server_filters_by_provider() { let server = httpmock::MockServer::start_async().await; - server.mock_async(|when, then| { - when.method("GET").path("/models"); - then.status(200) - .header("Content-Type", "application/json") - .body(serde_json::json!({ - "data": [ - { - "id": "model-a", - "provider": "alpha", - "family": "a", - "display_name": "Model A", - "limits": { "context_window": 8000 }, - "features": { "tools": false, "vision": false, "reasoning": false }, - "costs": {}, - "aliases": [], - "default": false - }, - { - "id": "model-b", - "provider": "beta", - "family": "b", - "display_name": "Model B", - "limits": { "context_window": 8000 }, - "features": { "tools": false, "vision": false, "reasoning": false }, - "costs": {}, - "aliases": [], - "default": false - } - ], - "meta": { "has_more": false } - }).to_string()); - }).await; + server + .mock_async(|when, then| { + when.method("GET").path("/models"); + then.status(200) + .header("Content-Type", "application/json") + .body( + serde_json::json!({ + "data": [ + { + "id": "model-a", + "provider": "alpha", + "family": "a", + "display_name": "Model A", + "limits": { "context_window": 8000 }, + "features": { "tools": false, "vision": false, "reasoning": false }, + "costs": {}, + "aliases": [], + "default": false + }, + { + "id": "model-b", + "provider": "beta", + "family": "b", + "display_name": "Model B", + "limits": { "context_window": 8000 }, + "features": { "tools": false, "vision": false, "reasoning": false }, + "costs": {}, + "aliases": [], + "default": false + } + ], + "meta": { "has_more": false } + }) + .to_string(), + ); + }) + .await; let client = reqwest::Client::new(); let models = fetch_models_from_server(&client, &server.url(""), Some("alpha")) @@ -1305,10 +1320,12 @@ mod tests { #[tokio::test] async fn fetch_models_from_server_error_on_failure() { let server = httpmock::MockServer::start_async().await; - server.mock_async(|when, then| { - when.method("GET").path("/models"); - then.status(500).body("internal error"); - }).await; + server + .mock_async(|when, then| { + when.method("GET").path("/models"); + then.status(500).body("internal error"); + }) + .await; let client = reqwest::Client::new(); let result = fetch_models_from_server(&client, &server.url(""), None).await; @@ -1320,18 +1337,23 @@ mod tests { #[tokio::test] async fn run_prompt_via_server_non_streaming() { let mock_server = httpmock::MockServer::start_async().await; - let mock = mock_server.mock_async(|when, then| { - when.method("POST").path("/completions"); - then.status(200) - .header("Content-Type", "application/json") - .body(serde_json::json!({ - "id": "msg_123", - "model": "test-model", - "content": "Hello world", - "stop_reason": "end_turn", - "usage": {"input_tokens": 10, "output_tokens": 5} - }).to_string()); - }).await; + let mock = mock_server + .mock_async(|when, then| { + when.method("POST").path("/completions"); + then.status(200) + .header("Content-Type", "application/json") + .body( + serde_json::json!({ + "id": "msg_123", + "model": "test-model", + "content": "Hello world", + "stop_reason": "end_turn", + "usage": {"input_tokens": 10, "output_tokens": 5} + }) + .to_string(), + ); + }) + .await; let server = ServerConnection { client: reqwest::Client::new(), @@ -1367,12 +1389,14 @@ event: message_stop\n\ data: {\"type\":\"message_stop\"}\n\ \n"; - let mock = mock_server.mock_async(|when, then| { - when.method("POST").path("/completions"); - then.status(200) - .header("Content-Type", "text/event-stream") - .body(sse_body); - }).await; + let mock = mock_server + .mock_async(|when, then| { + when.method("POST").path("/completions"); + then.status(200) + .header("Content-Type", "text/event-stream") + .body(sse_body); + }) + .await; let server = ServerConnection { client: reqwest::Client::new(), diff --git a/crates/arc-llm/src/providers/openai.rs b/crates/arc-llm/src/providers/openai.rs index 87f069511..1184984c7 100644 --- a/crates/arc-llm/src/providers/openai.rs +++ b/crates/arc-llm/src/providers/openai.rs @@ -1364,8 +1364,12 @@ mod tests { assert!(has_tool_calls); // reasoning + openai_message + text + function_call assert_eq!(parts.len(), 4); - assert!(matches!(&parts[0], ContentPart::Other { kind, .. } if kind == ContentPart::OPENAI_REASONING)); - assert!(matches!(&parts[1], ContentPart::Other { kind, data } if kind == ContentPart::OPENAI_MESSAGE && data["id"] == "msg_xyz")); + assert!( + matches!(&parts[0], ContentPart::Other { kind, .. } if kind == ContentPart::OPENAI_REASONING) + ); + assert!( + matches!(&parts[1], ContentPart::Other { kind, data } if kind == ContentPart::OPENAI_MESSAGE && data["id"] == "msg_xyz") + ); assert!(matches!(&parts[2], ContentPart::Text(t) if t == "Hello")); assert!(matches!(&parts[3], ContentPart::ToolCall(_))); } @@ -1521,10 +1525,7 @@ mod tests { } fn empty_sse_state() -> SseStreamState { - let http_resp = http::Response::builder() - .status(200) - .body("") - .unwrap(); + let http_resp = http::Response::builder().status(200).body("").unwrap(); let response = reqwest::Response::from(http_resp); SseStreamState { line_reader: crate::providers::common::LineReader::new(response, None), @@ -1556,7 +1557,9 @@ mod tests { ); assert_eq!(events.len(), 2); assert!(matches!(events[0], StreamEvent::ReasoningStart)); - assert!(matches!(events[1], StreamEvent::ReasoningDelta { ref delta } if delta == "Let me think")); + assert!( + matches!(events[1], StreamEvent::ReasoningDelta { ref delta } if delta == "Let me think") + ); } #[test] @@ -1565,24 +1568,20 @@ mod tests { // First delta: should emit ReasoningStart + ReasoningDelta let data1 = r#"{"type":"response.reasoning_text.delta","delta":"Step 1"}"#; - let events1 = process_sse_event( - &mut state, - Some("response.reasoning_text.delta"), - data1, - ); + let events1 = process_sse_event(&mut state, Some("response.reasoning_text.delta"), data1); assert_eq!(events1.len(), 2); assert!(matches!(events1[0], StreamEvent::ReasoningStart)); - assert!(matches!(events1[1], StreamEvent::ReasoningDelta { ref delta } if delta == "Step 1")); + assert!( + matches!(events1[1], StreamEvent::ReasoningDelta { ref delta } if delta == "Step 1") + ); // Second delta: should NOT emit duplicate ReasoningStart let data2 = r#"{"type":"response.reasoning_text.delta","delta":"Step 2"}"#; - let events2 = process_sse_event( - &mut state, - Some("response.reasoning_text.delta"), - data2, - ); + let events2 = process_sse_event(&mut state, Some("response.reasoning_text.delta"), data2); assert_eq!(events2.len(), 1); - assert!(matches!(events2[0], StreamEvent::ReasoningDelta { ref delta } if delta == "Step 2")); + assert!( + matches!(events2[0], StreamEvent::ReasoningDelta { ref delta } if delta == "Step 2") + ); } #[test] @@ -1591,11 +1590,7 @@ mod tests { state.emitted_reasoning_start = true; let data = r#"{"item":{"type":"reasoning","id":"rs_abc","summary":[]}}"#; - let events = process_sse_event( - &mut state, - Some("response.output_item.done"), - data, - ); + let events = process_sse_event(&mut state, Some("response.output_item.done"), data); assert_eq!(events.len(), 1); assert!(matches!(events[0], StreamEvent::ReasoningEnd)); assert!(!state.emitted_reasoning_start); diff --git a/crates/arc-slack/examples/slack_e2e.rs b/crates/arc-slack/examples/slack_e2e.rs index 3c0f91d0a..7ef3bcc85 100644 --- a/crates/arc-slack/examples/slack_e2e.rs +++ b/crates/arc-slack/examples/slack_e2e.rs @@ -104,9 +104,7 @@ async fn ask_question( let question_text = test_case.question.text.clone(); let is_freeform = test_case.question.question_type == QuestionType::Freeform; let interviewer_clone = Arc::clone(interviewer); - let ask_handle = tokio::spawn(async move { - interviewer_clone.ask(test_case.question).await - }); + let ask_handle = tokio::spawn(async move { interviewer_clone.ask(test_case.question).await }); tokio::time::sleep(std::time::Duration::from_millis(100)).await; @@ -190,7 +188,14 @@ async fn main() { let cases = test_cases(); for case in cases { - ask_question(case, &interviewer, &thread_registry, &slack_client, &channel).await; + ask_question( + case, + &interviewer, + &thread_registry, + &slack_client, + &channel, + ) + .await; } eprintln!("\nAll question types tested!"); diff --git a/crates/arc-slack/src/blocks.rs b/crates/arc-slack/src/blocks.rs index 8fa440cf6..327b47803 100644 --- a/crates/arc-slack/src/blocks.rs +++ b/crates/arc-slack/src/blocks.rs @@ -24,7 +24,9 @@ fn button(label: &str, value: &str, action_id: &str) -> Value { } pub fn answered_blocks(question_text: &str, answer_text: &str) -> Vec { - vec![text_block(&format!("~{question_text}~\n*Answer:* {answer_text}"))] + vec![text_block(&format!( + "~{question_text}~\n*Answer:* {answer_text}" + ))] } pub fn question_to_blocks(question_id: &str, question: &Question) -> Vec { diff --git a/crates/arc-slack/src/client.rs b/crates/arc-slack/src/client.rs index bdb979ff2..ca72b0529 100644 --- a/crates/arc-slack/src/client.rs +++ b/crates/arc-slack/src/client.rs @@ -143,11 +143,8 @@ mod tests { #[test] fn post_message_request_body_format() { - let body = build_post_message_body( - "#general", - &[serde_json::json!({"type": "section"})], - None, - ); + let body = + build_post_message_body("#general", &[serde_json::json!({"type": "section"})], None); assert_eq!(body["channel"], "#general"); assert_eq!(body["blocks"][0]["type"], "section"); assert!(body["thread_ts"].is_null()); diff --git a/crates/arc-slack/src/interaction.rs b/crates/arc-slack/src/interaction.rs index 8a8673aa1..be23c1f52 100644 --- a/crates/arc-slack/src/interaction.rs +++ b/crates/arc-slack/src/interaction.rs @@ -46,8 +46,7 @@ fn extract_checkbox_selections(question_id: &str, payload: &Value) -> Answer { let block_id = format!("{question_id}:checkboxes"); let action_id = format!("{question_id}:select"); - let selected = payload["state"]["values"][&block_id][&action_id]["selected_options"] - .as_array(); + let selected = payload["state"]["values"][&block_id][&action_id]["selected_options"].as_array(); match selected { Some(options) if !options.is_empty() => { diff --git a/crates/arc-sprites/src/lib.rs b/crates/arc-sprites/src/lib.rs index f9417cb5f..587949396 100644 --- a/crates/arc-sprites/src/lib.rs +++ b/crates/arc-sprites/src/lib.rs @@ -98,11 +98,7 @@ impl SpritesSandbox { /// Build args for `sprite exec -s [-o org] bash -c `. fn build_exec_args(&self, command: &str) -> Result, String> { let name = self.sprite_name()?; - let mut args = vec![ - "exec".to_string(), - "-s".to_string(), - name.to_string(), - ]; + let mut args = vec!["exec".to_string(), "-s".to_string(), name.to_string()]; if let Some(ref org) = self.config.org { args.push("-o".to_string()); args.push(org.clone()); @@ -180,8 +176,7 @@ impl Sandbox for SpritesSandbox { let url_refs: Vec<&str> = url_args.iter().map(|s| s.as_str()).collect(); let url_output = self.runner.run(&url_refs).await.map_err(|e| { let err = format!("Failed to get sprite URL: {e}"); - let duration_ms = - u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); + let duration_ms = u64::try_from(init_start.elapsed().as_millis()).unwrap_or(u64::MAX); self.emit(SandboxEvent::InitializeFailed { provider: PROVIDER.into(), error: err.clone(), @@ -273,7 +268,11 @@ impl Sandbox for SpritesSandbox { if let Some(vars) = env_vars { for (key, value) in vars { - parts.push(format!("export {}='{}';", key, value.replace('\'', "'\\''"))); + parts.push(format!( + "export {}='{}';", + key, + value.replace('\'', "'\\''") + )); } } @@ -816,10 +815,7 @@ mod tests { runner.queue_response("content\n", "", 0); let sandbox = sandbox_with_mock(runner); - sandbox - .read_file("/etc/hosts", None, None) - .await - .unwrap(); + sandbox.read_file("/etc/hosts", None, None).await.unwrap(); let recorded = commands.lock().unwrap(); let cmd = recorded[0].args.last().unwrap(); @@ -885,10 +881,7 @@ mod tests { let recorded = commands.lock().unwrap(); let cmd = recorded[0].args.last().unwrap(); - assert!( - cmd.contains("rm -f"), - "expected rm -f, got: {cmd}", - ); + assert!(cmd.contains("rm -f"), "expected rm -f, got: {cmd}",); } // ---- file_exists ---- @@ -980,11 +973,7 @@ mod tests { #[tokio::test] async fn glob_finds_files() { let runner = MockSpriteRunner::new(); - runner.queue_response( - "/home/sprite/src/main.rs\n/home/sprite/src/lib.rs\n", - "", - 0, - ); + runner.queue_response("/home/sprite/src/main.rs\n/home/sprite/src/lib.rs\n", "", 0); let sandbox = sandbox_with_mock(runner); let results = sandbox.glob("*.rs", Some("src")).await.unwrap(); @@ -999,8 +988,7 @@ mod tests { async fn download_file_to_local_writes_bytes() { let runner = MockSpriteRunner::new(); use base64::Engine; - let encoded = - base64::engine::general_purpose::STANDARD.encode(b"binary content"); + let encoded = base64::engine::general_purpose::STANDARD.encode(b"binary content"); runner.queue_response(&encoded, "", 0); let sandbox = sandbox_with_mock(runner); diff --git a/crates/arc-sprites/tests/e2e.rs b/crates/arc-sprites/tests/e2e.rs index 2b3f21c41..7743da8c1 100644 --- a/crates/arc-sprites/tests/e2e.rs +++ b/crates/arc-sprites/tests/e2e.rs @@ -42,7 +42,11 @@ async fn run_operations(sandbox: &SpritesSandbox) -> Result<(), String> { let result = sandbox .exec_command("echo hello", 30_000, None, None, None) .await?; - assert_eq!(result.exit_code, 0, "exec_command failed: {}", result.stderr); + assert_eq!( + result.exit_code, 0, + "exec_command failed: {}", + result.stderr + ); assert_eq!(result.stdout.trim(), "hello"); assert!(!result.timed_out); @@ -67,9 +71,7 @@ async fn run_operations(sandbox: &SpritesSandbox) -> Result<(), String> { .write_file("test-e2e/hello.txt", "Hello, Sprites!\nSecond line\n") .await?; - let content = sandbox - .read_file("test-e2e/hello.txt", None, None) - .await?; + let content = sandbox.read_file("test-e2e/hello.txt", None, None).await?; assert!( content.contains("Hello, Sprites!"), "read_file missing content: {content}", @@ -118,12 +120,8 @@ async fn run_operations(sandbox: &SpritesSandbox) -> Result<(), String> { ); // --- list_directory --- - sandbox - .write_file("test-e2e/sub/a.txt", "aaa") - .await?; - sandbox - .write_file("test-e2e/sub/b.txt", "bbb") - .await?; + sandbox.write_file("test-e2e/sub/a.txt", "aaa").await?; + sandbox.write_file("test-e2e/sub/b.txt", "bbb").await?; let entries = sandbox.list_directory("test-e2e/sub", None).await?; assert_eq!(entries.len(), 2, "expected 2 entries, got: {entries:?}"); assert_eq!(entries[0].name, "a.txt"); @@ -132,7 +130,10 @@ async fn run_operations(sandbox: &SpritesSandbox) -> Result<(), String> { // --- grep --- sandbox - .write_file("test-e2e/search/code.rs", "fn main() {\n println!(\"hello\");\n}\n") + .write_file( + "test-e2e/search/code.rs", + "fn main() {\n println!(\"hello\");\n}\n", + ) .await?; sandbox .write_file("test-e2e/search/data.txt", "no match here\n") @@ -188,10 +189,7 @@ async fn run_operations(sandbox: &SpritesSandbox) -> Result<(), String> { let downloaded = tokio::fs::read_to_string(&local_path) .await .map_err(|e| format!("read local: {e}"))?; - assert_eq!( - downloaded, download_content, - "download content mismatch", - ); + assert_eq!(downloaded, download_content, "download content mismatch",); Ok(()) } diff --git a/crates/arc-util/src/check_report.rs b/crates/arc-util/src/check_report.rs index c4540b561..d5f9a1282 100644 --- a/crates/arc-util/src/check_report.rs +++ b/crates/arc-util/src/check_report.rs @@ -297,7 +297,11 @@ mod tests { #[test] fn render_footer_text_when_provided() { let r = report(vec![pass_check("Test")]); - let out = r.render(&Styles::new(false), false, Some("Run with --live to probe.")); + let out = r.render( + &Styles::new(false), + false, + Some("Run with --live to probe."), + ); assert!(out.contains("Run with --live to probe.")); } diff --git a/crates/arc-util/src/telemetry/sender.rs b/crates/arc-util/src/telemetry/sender.rs index a37f105e8..e75ad9cc9 100644 --- a/crates/arc-util/src/telemetry/sender.rs +++ b/crates/arc-util/src/telemetry/sender.rs @@ -1,5 +1,5 @@ -use base64::Engine; use base64::engine::general_purpose::STANDARD; +use base64::Engine; use super::event::Track; diff --git a/crates/arc-workflows/src/cli/backend.rs b/crates/arc-workflows/src/cli/backend.rs index e9343c055..2f7fe9542 100644 --- a/crates/arc-workflows/src/cli/backend.rs +++ b/crates/arc-workflows/src/cli/backend.rs @@ -68,7 +68,8 @@ fn spawn_event_forwarder( .. } => { if !*is_error { - if let Some(path) = pending_tool_calls.lock().unwrap().remove(tool_call_id) { + if let Some(path) = pending_tool_calls.lock().unwrap().remove(tool_call_id) + { files_touched.lock().unwrap().insert(path); } } else { @@ -133,14 +134,7 @@ impl AgentApiBackend { node: &Node, sandbox: &Arc, ) -> Result { - Self::create_session_for( - &self.model, - self.provider, - node, - sandbox, - &self.env, - ) - .await + Self::create_session_for(&self.model, self.provider, node, sandbox, &self.env).await } async fn create_session_for( @@ -277,11 +271,17 @@ impl CodergenBackend for AgentApiBackend { Ok(resp) => ( resp, request.model.clone(), - request.provider.clone().unwrap_or_else(|| default_provider.clone()), + request + .provider + .clone() + .unwrap_or_else(|| default_provider.clone()), ), Err(sdk_err) if sdk_err.failover_eligible() && !fallback_chain.is_empty() => { let error_msg = sdk_err.to_string(); - let from_provider = request.provider.clone().unwrap_or_else(|| default_provider.clone()); + let from_provider = request + .provider + .clone() + .unwrap_or_else(|| default_provider.clone()); let from_model = request.model.clone(); let mut last_err = sdk_err; @@ -585,14 +585,19 @@ mod tests { #[test] fn agent_backend_stores_config() { - let backend = AgentApiBackend::new("claude-opus-4-6".to_string(), Provider::OpenAi, Vec::new()); + let backend = + AgentApiBackend::new("claude-opus-4-6".to_string(), Provider::OpenAi, Vec::new()); assert_eq!(backend.model, "claude-opus-4-6"); assert_eq!(backend.provider, Provider::OpenAi); } #[test] fn agent_backend_initializes_empty_sessions() { - let backend = AgentApiBackend::new("claude-opus-4-6".to_string(), Provider::Anthropic, Vec::new()); + let backend = AgentApiBackend::new( + "claude-opus-4-6".to_string(), + Provider::Anthropic, + Vec::new(), + ); assert!(backend.sessions.lock().unwrap().is_empty()); } diff --git a/crates/arc-workflows/src/cli/cli_backend.rs b/crates/arc-workflows/src/cli/cli_backend.rs index bdd4ec9ab..fc0ba12f6 100644 --- a/crates/arc-workflows/src/cli/cli_backend.rs +++ b/crates/arc-workflows/src/cli/cli_backend.rs @@ -73,7 +73,13 @@ async fn ensure_cli( // Check if the CLI is already installed (include ~/.local/bin for npm-installed CLIs) let version_check = sandbox - .exec_command(&format!("PATH=\"$HOME/.local/bin:$PATH\" {cli_name} --version"), 30_000, None, None, None) + .exec_command( + &format!("PATH=\"$HOME/.local/bin:$PATH\" {cli_name} --version"), + 30_000, + None, + None, + None, + ) .await .map_err(|e| ArcError::handler(format!("Failed to check {cli_name} version: {e}")))?; @@ -105,9 +111,23 @@ async fn ensure_cli( let node_installed = true; if install_result.exit_code != 0 { let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); - let output = if install_result.stderr.is_empty() { &install_result.stdout } else { &install_result.stderr }; - let detail: String = output.chars().rev().take(500).collect::>().into_iter().rev().collect(); - let error_msg = format!("{cli_name} install exited with code {}: {detail}", install_result.exit_code); + let output = if install_result.stderr.is_empty() { + &install_result.stdout + } else { + &install_result.stderr + }; + let detail: String = output + .chars() + .rev() + .take(500) + .collect::>() + .into_iter() + .rev() + .collect(); + let error_msg = format!( + "{cli_name} install exited with code {}: {detail}", + install_result.exit_code + ); emitter.emit(&WorkflowRunEvent::CliEnsureFailed { cli_name: cli_name.to_string(), provider: provider_str.to_string(), @@ -508,9 +528,8 @@ impl CodergenBackend for AgentCliBackend { tracing::info!(pid, "CLI process launched in background"); // 3c. Poll for completion - let poll_command = format!( - "[ -f {exit_code_path} ] && cat {exit_code_path} || echo running" - ); + let poll_command = + format!("[ -f {exit_code_path} ] && cat {exit_code_path} || echo running"); let poll_interval = std::time::Duration::from_secs(5); let exit_code: i32 = loop { tokio::time::sleep(poll_interval).await; @@ -542,8 +561,7 @@ impl CodergenBackend for AgentCliBackend { }; // 3d. Read results - let duration_ms = - u64::try_from(launch_start.elapsed().as_millis()).unwrap_or(u64::MAX); + let duration_ms = u64::try_from(launch_start.elapsed().as_millis()).unwrap_or(u64::MAX); let stdout_result = sandbox .exec_command(&format!("cat {stdout_path}"), 60_000, None, None, None) .await @@ -563,13 +581,7 @@ impl CodergenBackend for AgentCliBackend { // 3e. Cleanup temp files let _ = sandbox - .exec_command( - &format!("rm -f {tmp_prefix}_*"), - 30_000, - None, - None, - None, - ) + .exec_command(&format!("rm -f {tmp_prefix}_*"), 30_000, None, None, None) .await; if let Ok(json) = serde_json::to_string_pretty(&serde_json::json!({ @@ -585,11 +597,27 @@ impl CodergenBackend for AgentCliBackend { let _ = tokio::fs::write(stage_dir.join("cli_stdout.log"), &result.stdout).await; let _ = tokio::fs::write(stage_dir.join("cli_stderr.log"), &result.stderr).await; - let stderr: String = result.stderr.chars().rev().take(500).collect::>().into_iter().rev().collect(); + let stderr: String = result + .stderr + .chars() + .rev() + .take(500) + .collect::>() + .into_iter() + .rev() + .collect(); let detail = if !stderr.is_empty() { stderr } else { - let stdout: String = result.stdout.chars().rev().take(500).collect::>().into_iter().rev().collect(); + let stdout: String = result + .stdout + .chars() + .rev() + .take(500) + .collect::>() + .into_iter() + .rev() + .collect(); if !stdout.is_empty() { format!("stdout: {stdout}") } else { @@ -715,7 +743,10 @@ mod tests { #[test] fn agent_cli_for_provider() { - assert_eq!(AgentCli::for_provider(Provider::Anthropic), AgentCli::Claude); + assert_eq!( + AgentCli::for_provider(Provider::Anthropic), + AgentCli::Claude + ); assert_eq!(AgentCli::for_provider(Provider::OpenAi), AgentCli::Codex); assert_eq!(AgentCli::for_provider(Provider::Gemini), AgentCli::Gemini); assert_eq!(AgentCli::for_provider(Provider::Kimi), AgentCli::Codex); @@ -740,9 +771,9 @@ mod tests { // -- ensure_cli -- + use arc_agent::sandbox::{DirEntry, GrepOptions}; use std::collections::VecDeque; use std::sync::Mutex; - use arc_agent::sandbox::{DirEntry, GrepOptions}; /// Mock sandbox that returns pre-configured ExecResults in FIFO order. struct CliMockSandbox { @@ -765,13 +796,30 @@ mod tests { #[async_trait] impl Sandbox for CliMockSandbox { - async fn read_file(&self, _path: &str, _offset: Option, _limit: Option) -> Result { + async fn read_file( + &self, + _path: &str, + _offset: Option, + _limit: Option, + ) -> Result { Ok(String::new()) } - async fn write_file(&self, _path: &str, _content: &str) -> Result<(), String> { Ok(()) } - async fn delete_file(&self, _path: &str) -> Result<(), String> { Ok(()) } - async fn file_exists(&self, _path: &str) -> Result { Ok(false) } - async fn list_directory(&self, _path: &str, _depth: Option) -> Result, String> { Ok(vec![]) } + async fn write_file(&self, _path: &str, _content: &str) -> Result<(), String> { + Ok(()) + } + async fn delete_file(&self, _path: &str) -> Result<(), String> { + Ok(()) + } + async fn file_exists(&self, _path: &str) -> Result { + Ok(false) + } + async fn list_directory( + &self, + _path: &str, + _depth: Option, + ) -> Result, String> { + Ok(vec![]) + } async fn exec_command( &self, command: &str, @@ -781,25 +829,64 @@ mod tests { _cancel_token: Option, ) -> Result { self.commands.lock().unwrap().push(command.to_string()); - self.results.lock().unwrap().pop_front().ok_or_else(|| "no more mock results".to_string()) + self.results + .lock() + .unwrap() + .pop_front() + .ok_or_else(|| "no more mock results".to_string()) + } + async fn grep( + &self, + _pattern: &str, + _path: &str, + _options: &GrepOptions, + ) -> Result, String> { + Ok(vec![]) + } + async fn glob(&self, _pattern: &str, _path: Option<&str>) -> Result, String> { + Ok(vec![]) + } + async fn download_file_to_local(&self, _remote: &str, _local: &Path) -> Result<(), String> { + Ok(()) + } + async fn initialize(&self) -> Result<(), String> { + Ok(()) + } + async fn cleanup(&self) -> Result<(), String> { + Ok(()) + } + fn working_directory(&self) -> &str { + "/workspace" + } + fn platform(&self) -> &str { + "linux" + } + fn os_version(&self) -> String { + "Ubuntu 22.04".to_string() + } + async fn set_autostop_interval(&self, _minutes: i32) -> Result<(), String> { + Ok(()) } - async fn grep(&self, _pattern: &str, _path: &str, _options: &GrepOptions) -> Result, String> { Ok(vec![]) } - async fn glob(&self, _pattern: &str, _path: Option<&str>) -> Result, String> { Ok(vec![]) } - async fn download_file_to_local(&self, _remote: &str, _local: &Path) -> Result<(), String> { Ok(()) } - async fn initialize(&self) -> Result<(), String> { Ok(()) } - async fn cleanup(&self) -> Result<(), String> { Ok(()) } - fn working_directory(&self) -> &str { "/workspace" } - fn platform(&self) -> &str { "linux" } - fn os_version(&self) -> String { "Ubuntu 22.04".to_string() } - async fn set_autostop_interval(&self, _minutes: i32) -> Result<(), String> { Ok(()) } } fn ok_result() -> ExecResult { - ExecResult { exit_code: 0, stdout: String::new(), stderr: String::new(), timed_out: false, duration_ms: 10 } + ExecResult { + exit_code: 0, + stdout: String::new(), + stderr: String::new(), + timed_out: false, + duration_ms: 10, + } } fn fail_result(code: i32) -> ExecResult { - ExecResult { exit_code: code, stdout: String::new(), stderr: "error".to_string(), timed_out: false, duration_ms: 10 } + ExecResult { + exit_code: code, + stdout: String::new(), + stderr: "error".to_string(), + timed_out: false, + duration_ms: 10, + } } #[tokio::test] @@ -820,8 +907,8 @@ mod tests { async fn ensure_cli_installs_when_missing() { // version check fails, combined install succeeds let sandbox: Arc = Arc::new(CliMockSandbox::new(vec![ - fail_result(127), // claude --version - ok_result(), // combined node + npm install + fail_result(127), // claude --version + ok_result(), // combined node + npm install ])); let emitter = Arc::new(EventEmitter::new()); @@ -837,14 +924,17 @@ mod tests { #[tokio::test] async fn ensure_cli_fails_on_install_failure() { let sandbox: Arc = Arc::new(CliMockSandbox::new(vec![ - fail_result(127), // claude --version - fail_result(1), // combined install fails + fail_result(127), // claude --version + fail_result(1), // combined install fails ])); let emitter = Arc::new(EventEmitter::new()); let result = ensure_cli(AgentCli::Claude, Provider::Anthropic, &sandbox, &emitter).await; assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("install exited with code")); + assert!(result + .unwrap_err() + .to_string() + .contains("install exited with code")); } // -- Cycle 1: cli_command_for_provider -- diff --git a/crates/arc-workflows/src/cli/progress.rs b/crates/arc-workflows/src/cli/progress.rs index fbcd773c2..66b9df579 100644 --- a/crates/arc-workflows/src/cli/progress.rs +++ b/crates/arc-workflows/src/cli/progress.rs @@ -114,7 +114,11 @@ fn shorten_path(path: &str) -> String { fn tool_display_name(tool_name: &str, arguments: &serde_json::Value) -> String { let dim = Style::new().dim(); let arg = |key: &str| arguments.get(key).and_then(|v| v.as_str()); - let path_arg = || arg("path").or_else(|| arg("file_path")).map(|p| truncate(&shorten_path(p), 60)); + let path_arg = || { + arg("path") + .or_else(|| arg("file_path")) + .map(|p| truncate(&shorten_path(p), 60)) + }; let detail = match tool_name { "bash" | "shell" | "execute_command" => arg("command").map(|c| truncate(c, 60)), @@ -334,11 +338,7 @@ impl ProgressUI { } => { self.finish_stage(node_id, name, red_cross(), ""); let red = Style::new().red(); - self.insert_info_line(&format!( - "{} {}", - red.apply_to("Error:"), - failure.message, - )); + self.insert_info_line(&format!("{} {}", red.apply_to("Error:"), failure.message,)); } WorkflowRunEvent::ParallelStarted { .. } => { // The fork stage is the (only) active stage at this point. @@ -403,11 +403,7 @@ impl ProgressUI { } else { red_cross() }; - let msg = format!( - "{glyph} [{}/{total}] {}", - index + 1, - truncate(command, 60), - ); + let msg = format!("{glyph} [{}/{total}] {}", index + 1, truncate(command, 60),); match &self.renderer { ProgressRenderer::Tty(tty) => { let bar = if let Some(ref setup_bar) = self.setup_bar { @@ -480,9 +476,11 @@ impl ProgressUI { } => { let dur = format_duration_ms(*duration_ms); let detail = match (name, cpu, memory) { - (Some(n), Some(c), Some(m)) => { - Some(format!("{n} ({} cpu, {} GB)", format_number(*c), format_number(*m))) - } + (Some(n), Some(c), Some(m)) => Some(format!( + "{n} ({} cpu, {} GB)", + format_number(*c), + format_number(*m) + )), (Some(n), _, _) => Some(n.clone()), _ => None, }; @@ -497,7 +495,8 @@ impl ProgressUI { bar.set_prefix(dur); bar.finish_with_message(format!("Sandbox: {display_provider}")); if let Some(detail_str) = &detail { - let detail_bar = tty.multi.insert_after(&bar, ProgressBar::new_spinner()); + let detail_bar = + tty.multi.insert_after(&bar, ProgressBar::new_spinner()); detail_bar.set_style(style_sandbox_detail()); detail_bar.finish_with_message(detail_str.clone()); } @@ -579,9 +578,18 @@ impl ProgressUI { } } - fn on_cli_ensure_completed(&mut self, cli_name: &str, already_installed: bool, duration_ms: u64) { + fn on_cli_ensure_completed( + &mut self, + cli_name: &str, + already_installed: bool, + duration_ms: u64, + ) { let dur = format_duration_ms(duration_ms); - let status = if already_installed { "found" } else { "installed" }; + let status = if already_installed { + "found" + } else { + "installed" + }; match &self.renderer { ProgressRenderer::Tty(_) => { if let Some(bar) = self.cli_ensure_bar.take() { @@ -743,25 +751,23 @@ impl ProgressUI { ), ); } - AgentEvent::CompactionStarted { .. } => { - match &self.renderer { - ProgressRenderer::Tty(tty) => { - if let Some(stage) = self.active_stages.get_mut(stage_node_id) { - if let Some(old) = stage.compaction_bar.take() { - old.finish_and_clear(); - } - let bar = tty - .multi - .insert_after(stage.last_bar(), ProgressBar::new_spinner()); - bar.set_style(style_tool_running()); - bar.set_message("\u{27f3} compacting context\u{2026}"); - bar.enable_steady_tick(Duration::from_millis(100)); - stage.compaction_bar = Some(bar); + AgentEvent::CompactionStarted { .. } => match &self.renderer { + ProgressRenderer::Tty(tty) => { + if let Some(stage) = self.active_stages.get_mut(stage_node_id) { + if let Some(old) = stage.compaction_bar.take() { + old.finish_and_clear(); } + let bar = tty + .multi + .insert_after(stage.last_bar(), ProgressBar::new_spinner()); + bar.set_style(style_tool_running()); + bar.set_message("\u{27f3} compacting context\u{2026}"); + bar.enable_steady_tick(Duration::from_millis(100)); + stage.compaction_bar = Some(bar); } - ProgressRenderer::Plain => {} } - } + ProgressRenderer::Plain => {} + }, AgentEvent::CompactionCompleted { original_turn_count, preserved_turn_count, @@ -807,9 +813,7 @@ impl ProgressUI { ), ); } - AgentEvent::SubAgentSpawned { - agent_id, task, .. - } if self.verbose => { + AgentEvent::SubAgentSpawned { agent_id, task, .. } if self.verbose => { let dim = Style::new().dim(); let short_id = &agent_id[..agent_id.len().min(8)]; self.insert_info_line_for_stage( @@ -828,11 +832,7 @@ impl ProgressUI { .. } if self.verbose => { let short_id = &agent_id[..agent_id.len().min(8)]; - let glyph = if *success { - green_check() - } else { - red_cross() - }; + let glyph = if *success { green_check() } else { red_cross() }; self.insert_info_line_for_stage( stage_node_id, &format!("{glyph} subagent[{short_id}] ({turns_used} turns)"), @@ -864,10 +864,9 @@ impl ProgressUI { evicted.bar.finish_and_clear(); } } - let bar = tty.multi.insert_after( - stage.last_bar(), - ProgressBar::new_spinner(), - ); + let bar = tty + .multi + .insert_after(stage.last_bar(), ProgressBar::new_spinner()); bar.set_style(style_tool_running()); bar.set_message(display_name.clone()); bar.enable_steady_tick(Duration::from_millis(100)); @@ -892,10 +891,9 @@ impl ProgressUI { if let ProgressRenderer::Tty(tty) = &self.renderer { if let Some(stage) = self.active_stages.get_mut(&parent_id) { - let bar = tty.multi.insert_after( - stage.last_bar(), - ProgressBar::new_spinner(), - ); + let bar = tty + .multi + .insert_after(stage.last_bar(), ProgressBar::new_spinner()); bar.set_style(style_tool_running()); bar.set_message(branch.to_string()); bar.enable_steady_tick(Duration::from_millis(100)); @@ -912,7 +910,11 @@ impl ProgressUI { fn on_parallel_branch_completed(&mut self, branch: &str, duration_ms: u64, status: &str) { let succeeded = matches!(status, "success" | "partial_success"); - let glyph = if succeeded { green_check() } else { red_cross() }; + let glyph = if succeeded { + green_check() + } else { + red_cross() + }; let dur = format_duration_ms(duration_ms); let parent_id = match &self.parallel_parent { @@ -1102,7 +1104,10 @@ mod tests { let stage = ui.active_stages.get("fork1").unwrap(); assert_eq!(stage.tool_calls.len(), 1); assert_eq!(stage.tool_calls[0].tool_call_id, "security"); - assert!(matches!(stage.tool_calls[0].status, ToolCallStatus::Running)); + assert!(matches!( + stage.tool_calls[0].status, + ToolCallStatus::Running + )); // Branch completed → marks entry as succeeded ui.handle_event(&WorkflowRunEvent::ParallelBranchCompleted { @@ -1162,10 +1167,7 @@ mod tests { }); let stage = ui.active_stages.get("fork1").unwrap(); - assert!(matches!( - stage.tool_calls[0].status, - ToolCallStatus::Failed - )); + assert!(matches!(stage.tool_calls[0].status, ToolCallStatus::Failed)); } #[test] diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index 96d91f408..337d870f8 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -5,10 +5,10 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use anyhow::bail; -use tracing::debug; use arc_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox}; use arc_util::terminal::Styles; use chrono::{Local, Utc}; +use tracing::debug; use crate::checkpoint::Checkpoint; use crate::engine::{GitCheckpointMode, RunConfig, WorkflowRunEngine}; @@ -244,9 +244,9 @@ pub async fn run_command( }; let dot_dir = dot_path.parent().unwrap_or(std::path::Path::new(".")); let mut builder = WorkflowBuilder::new(); - builder.register_transform(Box::new( - crate::transform::FileInliningTransform::new(dot_dir.to_path_buf()), - )); + builder.register_transform(Box::new(crate::transform::FileInliningTransform::new( + dot_dir.to_path_buf(), + ))); let (mut graph, diagnostics) = builder.prepare(&source)?; apply_goal_override(&mut graph, args.goal.as_deref()); @@ -478,7 +478,11 @@ pub async fn run_command( .await .map_err(|e| anyhow::anyhow!("Failed to create Daytona client: {e}"))?; let config = daytona_config.clone().unwrap_or_default(); - let mut env = crate::daytona_sandbox::DaytonaSandbox::new(daytona_client, config, github_app.clone()); + let mut env = crate::daytona_sandbox::DaytonaSandbox::new( + daytona_client, + config, + github_app.clone(), + ); let emitter_cb = Arc::clone(&emitter); env.set_event_callback(Arc::new(move |event| { emitter_cb.emit(&crate::event::WorkflowRunEvent::Sandbox { event }); @@ -550,9 +554,7 @@ pub async fn run_command( if let Some(ref daytona) = daytona_sandbox_ref { match daytona.create_ssh_access().await { Ok(ssh_command) => { - emitter.emit(&crate::event::WorkflowRunEvent::SshAccessReady { - ssh_command, - }); + emitter.emit(&crate::event::WorkflowRunEvent::SshAccessReady { ssh_command }); } Err(e) => { eprintln!( @@ -652,11 +654,7 @@ pub async fn run_command( .unwrap_or(Provider::Anthropic); // Resolve fallback chain from config - let fallback_chain = resolve_fallback_chain( - provider_enum, - &model, - run_cfg.as_ref(), - ); + let fallback_chain = resolve_fallback_chain(provider_enum, &model, run_cfg.as_ref()); // 7. Build engine let sandbox_env: HashMap = run_cfg @@ -671,8 +669,9 @@ pub async fn run_command( if dry_run_mode { None } else { - let api = AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone()) - .with_env(sandbox_env.clone()); + let api = + AgentApiBackend::new(model.clone(), provider_enum, fallback_chain.clone()) + .with_env(sandbox_env.clone()); let cli = AgentCliBackend::new(model.clone(), provider_enum) .with_env(sandbox_env.clone()); Some(Box::new(BackendRouter::new(Box::new(api), cli))) @@ -835,7 +834,12 @@ pub async fn run_command( )) ); } else { - eprintln!("{}", styles.dim.apply_to(format!("Tokens: {}", format_tokens_human(total_tokens)))); + eprintln!( + "{}", + styles + .dim + .apply_to(format!("Tokens: {}", format_tokens_human(total_tokens))) + ); } if acc.total_cache_read_tokens > 0 { eprintln!( @@ -875,10 +879,7 @@ pub async fn run_command( styles.bold.apply_to("Info:") ); } else { - eprintln!( - "\n{} sandbox preserved", - styles.bold.apply_to("Info:") - ); + eprintln!("\n{} sandbox preserved", styles.bold.apply_to("Info:")); } } else if let Err(e) = sandbox.cleanup().await { tracing::warn!(error = %e, "Sandbox cleanup failed"); @@ -1042,8 +1043,8 @@ async fn run_from_branch( .map_err(|e| anyhow::anyhow!("failed to attach worktree to {run_branch}: {e}"))?; std::env::set_current_dir(&worktree_path)?; - let base_sha = crate::git::MetadataStore::read_manifest(&original_cwd, &run_id)? - .and_then(|m| m.base_sha); + let base_sha = + crate::git::MetadataStore::read_manifest(&original_cwd, &run_id)?.and_then(|m| m.base_sha); // Build minimal sandbox (local only for now) let emitter = Arc::new(EventEmitter::new()); @@ -1239,13 +1240,27 @@ async fn run_preflight( status: CheckStatus::Pass, summary: graph.name.clone(), details: vec![ - CheckDetail { text: format!("Nodes: {}", graph.nodes.len()) }, - CheckDetail { text: format!("Edges: {}", graph.edges.len()) }, - CheckDetail { text: format!("Goal: {}", graph.goal()) }, - CheckDetail { text: format!("Model: {model}") }, - CheckDetail { text: format!("Provider: {}", provider.as_deref().unwrap_or("anthropic")) }, - CheckDetail { text: format!("Setup commands: {setup_command_count}") }, - CheckDetail { text: format!("Git clean: {git_clean}") }, + CheckDetail { + text: format!("Nodes: {}", graph.nodes.len()), + }, + CheckDetail { + text: format!("Edges: {}", graph.edges.len()), + }, + CheckDetail { + text: format!("Goal: {}", graph.goal()), + }, + CheckDetail { + text: format!("Model: {model}"), + }, + CheckDetail { + text: format!("Provider: {}", provider.as_deref().unwrap_or("anthropic")), + }, + CheckDetail { + text: format!("Setup commands: {setup_command_count}"), + }, + CheckDetail { + text: format!("Git clean: {git_clean}"), + }, ], remediation: None, }); @@ -1267,7 +1282,8 @@ async fn run_preflight( SandboxProvider::Daytona => match daytona_sdk::Client::new().await { Ok(daytona_client) => { let config = daytona_config.unwrap_or_default(); - let env = crate::daytona_sandbox::DaytonaSandbox::new(daytona_client, config, github_app); + let env = + crate::daytona_sandbox::DaytonaSandbox::new(daytona_client, config, github_app); Ok(Arc::new(env) as Arc) } Err(e) => Err(format!("Daytona client creation failed: {e}")), @@ -1296,7 +1312,9 @@ async fn run_preflight( name: "Sandbox".into(), status: CheckStatus::Error, summary: "failed".into(), - details: vec![CheckDetail { text: format!("Provider: {sandbox_provider}") }], + details: vec![CheckDetail { + text: format!("Provider: {sandbox_provider}"), + }], remediation: Some(format!("Sandbox init failed: {e}")), }); false @@ -1307,7 +1325,9 @@ async fn run_preflight( name: "Sandbox".into(), status: CheckStatus::Error, summary: "failed".into(), - details: vec![CheckDetail { text: format!("Provider: {sandbox_provider}") }], + details: vec![CheckDetail { + text: format!("Provider: {sandbox_provider}"), + }], remediation: Some(e), }); false @@ -1319,7 +1339,9 @@ async fn run_preflight( name: "Sandbox".into(), status: CheckStatus::Pass, summary: sandbox_provider.to_string(), - details: vec![CheckDetail { text: format!("Provider: {sandbox_provider}") }], + details: vec![CheckDetail { + text: format!("Provider: {sandbox_provider}"), + }], remediation: None, }); } @@ -1327,11 +1349,7 @@ async fn run_preflight( // 3. LLM client check let llm_ok = match arc_llm::client::Client::from_env().await { Ok(c) => { - let names: Vec = c - .provider_names() - .iter() - .map(|s| s.to_string()) - .collect(); + let names: Vec = c.provider_names().iter().map(|s| s.to_string()).collect(); if names.is_empty() { checks.push(CheckResult { name: "LLM providers".into(), @@ -1469,7 +1487,10 @@ async fn generate_retro( // Run retro agent session eprintln!("\n{}", styles.bold.apply_to("=== Retro ===")); - eprintln!("{}", styles.dim.apply_to(format!("Running retro ({model})..."))); + eprintln!( + "{}", + styles.dim.apply_to(format!("Running retro ({model})...")) + ); let retro_start = std::time::Instant::now(); let narrative_result = if dry_run_mode { Ok(crate::retro_agent::dry_run_narrative()) @@ -1491,10 +1512,7 @@ async fn generate_retro( .as_ref() .map(|s| s.to_string()) .unwrap_or_else(|| "unknown".to_string()); - let outcome_str = retro - .outcome - .as_deref() - .unwrap_or("No outcome recorded"); + let outcome_str = retro.outcome.as_deref().unwrap_or("No outcome recorded"); let line1_content = format!("Retro: {smoothness_str} \u{2014} {outcome_str}"); let term_width = console::Term::stderr().size().1 as usize; let dur_len = retro_dur.len(); @@ -1502,19 +1520,17 @@ async fn generate_retro( eprintln!( "{} {}{:pad1$}{}", styles.bold.apply_to("Retro:"), - styles.dim.apply_to(format!("{smoothness_str} \u{2014} {outcome_str}")), + styles + .dim + .apply_to(format!("{smoothness_str} \u{2014} {outcome_str}")), "", styles.dim.apply_to(&retro_dur), ); // Line 2: friction + open items (only if non-zero) - let friction_count = retro - .friction_points - .as_ref() - .map(|v| v.len()) - .unwrap_or(0); - let open_count = - retro.open_items.as_ref().map(|v| v.len()).unwrap_or(0); + let friction_count = + retro.friction_points.as_ref().map(|v| v.len()).unwrap_or(0); + let open_count = retro.open_items.as_ref().map(|v| v.len()).unwrap_or(0); if friction_count > 0 || open_count > 0 { let mut parts = Vec::new(); if friction_count > 0 { @@ -1537,10 +1553,7 @@ async fn generate_retro( } // Line 3: file path - let retro_path = format!( - "{}/retro.json", - super::tilde_path(logs_dir) - ); + let retro_path = format!("{}/retro.json", super::tilde_path(logs_dir)); eprintln!( " {} {}", styles.dim.apply_to("Retro saved to"), @@ -1572,9 +1585,10 @@ mod tests { fn apply_goal_override_replaces_graph_goal() { use crate::graph::types::{AttrValue, Graph}; let mut graph = Graph::new("test"); - graph - .attrs - .insert("goal".to_string(), AttrValue::String("original".to_string())); + graph.attrs.insert( + "goal".to_string(), + AttrValue::String("original".to_string()), + ); apply_goal_override(&mut graph, Some("CLI goal")); assert_eq!(graph.goal(), "CLI goal"); } @@ -1583,9 +1597,10 @@ mod tests { fn apply_goal_override_noop_when_none() { use crate::graph::types::{AttrValue, Graph}; let mut graph = Graph::new("test"); - graph - .attrs - .insert("goal".to_string(), AttrValue::String("original".to_string())); + graph.attrs.insert( + "goal".to_string(), + AttrValue::String("original".to_string()), + ); apply_goal_override(&mut graph, None); assert_eq!(graph.goal(), "original"); } diff --git a/crates/arc-workflows/src/cli/run_config.rs b/crates/arc-workflows/src/cli/run_config.rs index 2b95bdf7a..d65dcbe3a 100644 --- a/crates/arc-workflows/src/cli/run_config.rs +++ b/crates/arc-workflows/src/cli/run_config.rs @@ -189,11 +189,7 @@ pub fn load_run_config(path: &Path) -> anyhow::Result { /// Only whole-value references are supported (no partial interpolation). /// Missing host env vars produce a hard error. fn resolve_sandbox_env(config: &mut WorkflowRunConfig) -> anyhow::Result<()> { - if let Some(env) = config - .sandbox - .as_mut() - .and_then(|s| s.env.as_mut()) - { + if let Some(env) = config.sandbox.as_mut().and_then(|s| s.env.as_mut()) { resolve_env_refs(env)?; } Ok(()) @@ -211,9 +207,7 @@ pub fn resolve_env_refs(env: &mut HashMap) -> anyhow::Result<()> .and_then(|s| s.strip_suffix('}')) { *value = std::env::var(var_name).with_context(|| { - format!( - "sandbox.env.{key}: host environment variable {var_name:?} is not set" - ) + format!("sandbox.env.{key}: host environment variable {var_name:?} is not set") })?; } } @@ -624,8 +618,11 @@ dockerfile = { path = "./Dockerfile" } fn load_run_config_resolves_dockerfile_path() { let dir = tempfile::tempdir().unwrap(); let dockerfile_path = dir.path().join("Dockerfile"); - std::fs::write(&dockerfile_path, "FROM rust:1.85-slim-bookworm\nRUN apt-get update") - .unwrap(); + std::fs::write( + &dockerfile_path, + "FROM rust:1.85-slim-bookworm\nRUN apt-get update", + ) + .unwrap(); let toml_path = dir.path().join("run.toml"); std::fs::write( @@ -1205,19 +1202,13 @@ anthropic = ["gemini"] llm: Some(LlmConfig { model: None, provider: Some("anthropic".into()), - fallbacks: Some(HashMap::from([( - "anthropic".into(), - vec!["openai".into()], - )])), + fallbacks: Some(HashMap::from([("anthropic".into(), vec!["openai".into()])])), }), ..RunDefaults::default() }; cfg.apply_defaults(&defaults); let llm = cfg.llm.unwrap(); - assert_eq!( - llm.fallbacks.unwrap()["anthropic"], - vec!["gemini"] - ); + assert_eq!(llm.fallbacks.unwrap()["anthropic"], vec!["gemini"]); } #[test] @@ -1237,19 +1228,13 @@ model = "opus" llm: Some(LlmConfig { model: None, provider: Some("anthropic".into()), - fallbacks: Some(HashMap::from([( - "anthropic".into(), - vec!["openai".into()], - )])), + fallbacks: Some(HashMap::from([("anthropic".into(), vec!["openai".into()])])), }), ..RunDefaults::default() }; cfg.apply_defaults(&defaults); let llm = cfg.llm.unwrap(); - assert_eq!( - llm.fallbacks.unwrap()["anthropic"], - vec!["openai"] - ); + assert_eq!(llm.fallbacks.unwrap()["anthropic"], vec!["openai"]); } #[tokio::test] @@ -1461,10 +1446,7 @@ exclude_globs = ["**/dist/**", "**/.cache/**"] .unwrap(); let defaults = RunDefaults { checkpoint: CheckpointConfig { - exclude_globs: vec![ - "**/.cache/**".into(), - "**/node_modules/**".into(), - ], + exclude_globs: vec!["**/.cache/**".into(), "**/node_modules/**".into()], }, ..RunDefaults::default() }; @@ -1492,10 +1474,7 @@ graph = "w.dot" ..RunDefaults::default() }; cfg.apply_defaults(&defaults); - assert_eq!( - cfg.checkpoint.exclude_globs, - vec!["**/node_modules/**"] - ); + assert_eq!(cfg.checkpoint.exclude_globs, vec!["**/node_modules/**"]); } #[test] @@ -1513,10 +1492,7 @@ exclude_globs = ["**/dist/**"] .unwrap(); let defaults = RunDefaults::default(); cfg.apply_defaults(&defaults); - assert_eq!( - cfg.checkpoint.exclude_globs, - vec!["**/dist/**"] - ); + assert_eq!(cfg.checkpoint.exclude_globs, vec!["**/dist/**"]); } #[test] @@ -1574,9 +1550,7 @@ provider = "daytona" #[test] fn resolve_env_refs_host_var() { std::env::set_var("ARC_TEST_RESOLVE_VAR", "secret123"); - let mut env = HashMap::from([ - ("MY_KEY".into(), "${env.ARC_TEST_RESOLVE_VAR}".into()), - ]); + let mut env = HashMap::from([("MY_KEY".into(), "${env.ARC_TEST_RESOLVE_VAR}".into())]); resolve_env_refs(&mut env).unwrap(); assert_eq!(env["MY_KEY"], "secret123"); std::env::remove_var("ARC_TEST_RESOLVE_VAR"); @@ -1584,9 +1558,10 @@ provider = "daytona" #[test] fn resolve_env_refs_missing_var_errors() { - let mut env = HashMap::from([ - ("MY_KEY".into(), "${env.ARC_TEST_NONEXISTENT_VAR_12345}".into()), - ]); + let mut env = HashMap::from([( + "MY_KEY".into(), + "${env.ARC_TEST_NONEXISTENT_VAR_12345}".into(), + )]); let err = resolve_env_refs(&mut env).unwrap_err(); assert!( err.to_string().contains("ARC_TEST_NONEXISTENT_VAR_12345"), @@ -1596,9 +1571,7 @@ provider = "daytona" #[test] fn resolve_env_refs_partial_not_interpolated() { - let mut env = HashMap::from([ - ("MIXED".into(), "prefix_${env.HOME}_suffix".into()), - ]); + let mut env = HashMap::from([("MIXED".into(), "prefix_${env.HOME}_suffix".into())]); // Partial interpolation is not supported — value is left as-is resolve_env_refs(&mut env).unwrap(); assert_eq!(env["MIXED"], "prefix_${env.HOME}_suffix"); diff --git a/crates/arc-workflows/src/cli/runs.rs b/crates/arc-workflows/src/cli/runs.rs index 879d52d95..4b5c33413 100644 --- a/crates/arc-workflows/src/cli/runs.rs +++ b/crates/arc-workflows/src/cli/runs.rs @@ -337,7 +337,9 @@ mod tests { "edge_count": 1, "labels": { "env": "prod" } })), - Some(serde_json::json!({ "timestamp": "2026-01-01T12:01:00Z", "status": "success", "duration_ms": 60000 })), + Some( + serde_json::json!({ "timestamp": "2026-01-01T12:01:00Z", "status": "success", "duration_ms": 60000 }), + ), false, ); @@ -523,7 +525,9 @@ mod tests { "node_count": 1, "edge_count": 0 })), - Some(serde_json::json!({ "timestamp": "2025-01-01T12:01:00Z", "status": "success", "duration_ms": 60000 })), + Some( + serde_json::json!({ "timestamp": "2025-01-01T12:01:00Z", "status": "success", "duration_ms": 60000 }), + ), false, ); @@ -557,7 +561,9 @@ mod tests { "node_count": 1, "edge_count": 0 })), - Some(serde_json::json!({ "timestamp": "2025-01-01T12:01:00Z", "status": "success", "duration_ms": 60000 })), + Some( + serde_json::json!({ "timestamp": "2025-01-01T12:01:00Z", "status": "success", "duration_ms": 60000 }), + ), false, ); @@ -573,7 +579,9 @@ mod tests { "node_count": 1, "edge_count": 0 })), - Some(serde_json::json!({ "timestamp": "2026-03-01T12:01:00Z", "status": "success", "duration_ms": 60000 })), + Some( + serde_json::json!({ "timestamp": "2026-03-01T12:01:00Z", "status": "success", "duration_ms": 60000 }), + ), false, ); diff --git a/crates/arc-workflows/src/cli/validate.rs b/crates/arc-workflows/src/cli/validate.rs index c3d3ceefd..86f46ac29 100644 --- a/crates/arc-workflows/src/cli/validate.rs +++ b/crates/arc-workflows/src/cli/validate.rs @@ -17,9 +17,7 @@ pub fn validate_command(args: &ValidateArgs, styles: &Styles) -> anyhow::Result< eprintln!( "{} ({} nodes, {} edges)", - styles - .bold - .apply_to(format!("Workflow: {}", graph.name)), + styles.bold.apply_to(format!("Workflow: {}", graph.name)), graph.nodes.len(), graph.edges.len(), ); diff --git a/crates/arc-workflows/src/conclusion.rs b/crates/arc-workflows/src/conclusion.rs index d18637527..63dde444a 100644 --- a/crates/arc-workflows/src/conclusion.rs +++ b/crates/arc-workflows/src/conclusion.rs @@ -53,10 +53,7 @@ mod tests { assert_eq!(loaded.status, crate::outcome::StageStatus::Success); assert_eq!(loaded.duration_ms, 12345); assert!(loaded.failure_reason.is_none()); - assert_eq!( - loaded.final_git_commit_sha.as_deref(), - Some("deadbeef") - ); + assert_eq!(loaded.final_git_commit_sha.as_deref(), Some("deadbeef")); } #[test] diff --git a/crates/arc-workflows/src/condition.rs b/crates/arc-workflows/src/condition.rs index ed5c9a84b..4fc8aba63 100644 --- a/crates/arc-workflows/src/condition.rs +++ b/crates/arc-workflows/src/condition.rs @@ -54,17 +54,17 @@ enum Op { #[derive(Debug, Clone, PartialEq)] enum Token { Word(String), - OpEq, // = - OpNotEq, // != - OpGt, // > - OpLt, // < - OpGte, // >= - OpLte, // <= - And, // && - Or, // || - Not, // ! - Contains, // contains - Matches, // matches + OpEq, // = + OpNotEq, // != + OpGt, // > + OpLt, // < + OpGte, // >= + OpLte, // <= + And, // && + Or, // || + Not, // ! + Contains, // contains + Matches, // matches } fn tokenize(input: &str) -> Result, ArcError> { @@ -89,21 +89,57 @@ fn tokenize(input: &str) -> Result, ArcError> { if i + 1 < len { let two = format!("{}{}", chars[i], chars[i + 1]); match two.as_str() { - "&&" => { tokens.push(Token::And); i += 2; continue; } - "||" => { tokens.push(Token::Or); i += 2; continue; } - "!=" => { tokens.push(Token::OpNotEq); i += 2; continue; } - ">=" => { tokens.push(Token::OpGte); i += 2; continue; } - "<=" => { tokens.push(Token::OpLte); i += 2; continue; } + "&&" => { + tokens.push(Token::And); + i += 2; + continue; + } + "||" => { + tokens.push(Token::Or); + i += 2; + continue; + } + "!=" => { + tokens.push(Token::OpNotEq); + i += 2; + continue; + } + ">=" => { + tokens.push(Token::OpGte); + i += 2; + continue; + } + "<=" => { + tokens.push(Token::OpLte); + i += 2; + continue; + } _ => {} } } // Single-char operators match chars[i] { - '=' => { tokens.push(Token::OpEq); i += 1; continue; } - '>' => { tokens.push(Token::OpGt); i += 1; continue; } - '<' => { tokens.push(Token::OpLt); i += 1; continue; } - '!' => { tokens.push(Token::Not); i += 1; continue; } + '=' => { + tokens.push(Token::OpEq); + i += 1; + continue; + } + '>' => { + tokens.push(Token::OpGt); + i += 1; + continue; + } + '<' => { + tokens.push(Token::OpLt); + i += 1; + continue; + } + '!' => { + tokens.push(Token::Not); + i += 1; + continue; + } _ => {} } @@ -264,18 +300,15 @@ impl Parser { if op == Op::Eq || op == Op::NotEq { String::new() } else { - return Err(ArcError::Parse( - "expected value after operator".to_string(), - )); + return Err(ArcError::Parse("expected value after operator".to_string())); } } }; // Validate regex at parse time if op == Op::Matches { - regex::Regex::new(&value).map_err(|e| { - ArcError::Parse(format!("invalid regex pattern '{value}': {e}")) - })?; + regex::Regex::new(&value) + .map_err(|e| ArcError::Parse(format!("invalid regex pattern '{value}': {e}")))?; } Ok(ConditionExpr::Clause(Clause { key, op, value })) @@ -333,11 +366,7 @@ fn resolve_key(key: &str, outcome: &Outcome, context: &Context) -> String { .map_or_else(String::new, |val| json_value_to_string(&val)) } -fn resolve_key_value( - key: &str, - outcome: &Outcome, - context: &Context, -) -> serde_json::Value { +fn resolve_key_value(key: &str, outcome: &Outcome, context: &Context) -> serde_json::Value { if key == keys::OUTCOME { return serde_json::Value::String(outcome.status.to_string()); } @@ -383,9 +412,7 @@ fn eval_expr(expr: &ConditionExpr, outcome: &Outcome, context: &Context) -> bool } children.iter().all(|c| eval_expr(c, outcome, context)) } - ConditionExpr::Or(children) => { - children.iter().any(|c| eval_expr(c, outcome, context)) - } + ConditionExpr::Or(children) => children.iter().any(|c| eval_expr(c, outcome, context)), ConditionExpr::Not(inner) => !eval_expr(inner, outcome, context), ConditionExpr::Clause(clause) => eval_clause(clause, outcome, context), } @@ -426,9 +453,9 @@ fn eval_clause(clause: &Clause, outcome: &Outcome, context: &Context) -> bool { Op::Contains => { let raw = resolve_key_value(&clause.key, outcome, context); match &raw { - serde_json::Value::Array(arr) => arr.iter().any(|elem| { - json_value_to_string(elem) == clause.value - }), + serde_json::Value::Array(arr) => arr + .iter() + .any(|elem| json_value_to_string(elem) == clause.value), _ => { let s = json_value_to_string(&raw); s.contains(&clause.value) @@ -747,7 +774,11 @@ mod tests { context.set("score", serde_json::json!(90)); assert!(evaluate_condition("context.score > 80", &outcome, &context)); context.set("score", serde_json::json!(70)); - assert!(!evaluate_condition("context.score > 80", &outcome, &context)); + assert!(!evaluate_condition( + "context.score > 80", + &outcome, + &context + )); } #[test] @@ -755,7 +786,11 @@ mod tests { let outcome = make_outcome(StageStatus::Success); let context = Context::new(); context.set("score", serde_json::json!(80)); - assert!(evaluate_condition("context.score >= 80", &outcome, &context)); + assert!(evaluate_condition( + "context.score >= 80", + &outcome, + &context + )); } #[test] @@ -763,7 +798,11 @@ mod tests { let outcome = make_outcome(StageStatus::Success); let context = Context::new(); context.set("score", serde_json::json!(80)); - assert!(evaluate_condition("context.score <= 80", &outcome, &context)); + assert!(evaluate_condition( + "context.score <= 80", + &outcome, + &context + )); } #[test] @@ -924,11 +963,7 @@ mod tests { context.set("b", serde_json::json!("2")); context.set("c", serde_json::json!("3")); // a=1 is false, b=2 is true => AND is false; c=3 is true => OR is true - assert!(evaluate_condition( - "a=1 && b=2 || c=3", - &outcome, - &context - )); + assert!(evaluate_condition("a=1 && b=2 || c=3", &outcome, &context)); } #[test] @@ -940,11 +975,7 @@ mod tests { context.set("b", serde_json::json!("2")); context.set("c", serde_json::json!("0")); // a=1 false; b=2 true, c=3 false => AND false; OR false - assert!(!evaluate_condition( - "a=1 || b=2 && c=3", - &outcome, - &context - )); + assert!(!evaluate_condition("a=1 || b=2 && c=3", &outcome, &context)); } #[test] diff --git a/crates/arc-workflows/src/context/keys.rs b/crates/arc-workflows/src/context/keys.rs index 41363ccda..36e1f3c66 100644 --- a/crates/arc-workflows/src/context/keys.rs +++ b/crates/arc-workflows/src/context/keys.rs @@ -2,7 +2,6 @@ /// /// All context keys used across the engine, handlers, and preamble are /// defined here to prevent typos and improve discoverability. - use std::fmt; use std::str::FromStr; @@ -160,10 +159,7 @@ mod tests { #[test] fn thread_current_node_key_formats_correctly() { - assert_eq!( - thread_current_node_key("main"), - "thread.main.current_node" - ); + assert_eq!(thread_current_node_key("main"), "thread.main.current_node"); } #[test] diff --git a/crates/arc-workflows/src/daytona_sandbox.rs b/crates/arc-workflows/src/daytona_sandbox.rs index c941e2402..8403fbe80 100644 --- a/crates/arc-workflows/src/daytona_sandbox.rs +++ b/crates/arc-workflows/src/daytona_sandbox.rs @@ -103,9 +103,7 @@ impl<'de> Deserialize<'de> for DaytonaNetwork { let cidrs: Vec = map.next_value()?; if cidrs.is_empty() { - return Err(de::Error::custom( - "allow_list must not be empty", - )); + return Err(de::Error::custom("allow_list must not be empty")); } if let Some(extra) = map.next_key::()? { @@ -481,8 +479,8 @@ impl Sandbox for DaytonaSandbox { // Resolve clone credentials via GitHub App or fall back to no auth let (username, password) = match &self.github_app { Some(creds) => { - let (owner, repo) = - crate::github_app::parse_github_owner_repo(&url).map_err(|e| { + let (owner, repo) = crate::github_app::parse_github_owner_repo(&url) + .map_err(|e| { let err = format!("Failed to parse GitHub URL for clone: {e}"); self.emit(SandboxEvent::GitCloneFailed { url: url.clone(), @@ -721,11 +719,8 @@ impl Sandbox for DaytonaSandbox { .map_err(|e| format!("Failed to refresh GitHub App token: {e}"))?; if let Some(token) = password { - let auth_url = origin_url.replacen( - "https://", - &format!("https://x-access-token:{token}@"), - 1, - ); + let auth_url = + origin_url.replacen("https://", &format!("https://x-access-token:{token}@"), 1); let cmd = format!( "git -c maintenance.auto=0 remote set-url origin '{}'", auth_url.replace('\'', "'\\''"), @@ -920,12 +915,12 @@ impl Sandbox for DaytonaSandbox { // Wrap with `bash -c` so pipes, env vars, and shell features work. // The Daytona API uses direct exec, not a shell. let wrapped = wrap_bash_command(&command_with_env); - + let timeout_duration = std::time::Duration::from_millis(timeout_ms + 5000); // 5s grace period let token = cancel_token.unwrap_or_default(); - + let exec_future = process_svc.execute_command(&wrapped, options); - + let result = tokio::select! { res = exec_future => { res.map_err(|e| format!("Failed to execute command: {e}"))? @@ -1215,8 +1210,7 @@ mod tests { #[test] fn network_unknown_key_error() { - let err = - toml::from_str::(r#"network = { mode = "block" }"#).unwrap_err(); + let err = toml::from_str::(r#"network = { mode = "block" }"#).unwrap_err(); let msg = err.to_string(); assert!( msg.contains(r#"unknown key "mode""#), @@ -1228,16 +1222,12 @@ mod tests { fn network_empty_table_error() { let err = toml::from_str::("network = {}").unwrap_err(); let msg = err.to_string(); - assert!( - msg.contains("empty table"), - "unexpected error: {msg}" - ); + assert!(msg.contains("empty table"), "unexpected error: {msg}"); } #[test] fn network_empty_allow_list_error() { - let err = - toml::from_str::("network = { allow_list = [] }").unwrap_err(); + let err = toml::from_str::("network = { allow_list = [] }").unwrap_err(); let msg = err.to_string(); assert!( msg.contains("allow_list must not be empty"), diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs index 8427491b1..a8fcc0b36 100644 --- a/crates/arc-workflows/src/engine.rs +++ b/crates/arc-workflows/src/engine.rs @@ -25,8 +25,8 @@ use crate::graph::{Edge, Graph, Node}; use crate::handler::{EngineServices, HandlerRegistry}; use crate::hook::{HookContext, HookDecision, HookEvent, HookRunner}; use crate::interviewer::Interviewer; -use crate::outcome::{Outcome, StageStatus}; use crate::millis_u64; +use crate::outcome::{Outcome, StageStatus}; use crate::preamble::build_preamble; /// Classify the failure mode of a completed outcome. @@ -283,7 +283,11 @@ pub fn resolve_thread_id( // --- Run directory helpers (spec 5.6) --- /// Write manifest.json at the start of a workflow run. Returns the manifest. -fn write_manifest(logs_root: &Path, graph: &Graph, config: &RunConfig) -> crate::manifest::Manifest { +fn write_manifest( + logs_root: &Path, + graph: &Graph, + config: &RunConfig, +) -> crate::manifest::Manifest { let workflow_name = if graph.name.is_empty() { "unnamed".to_string() } else { @@ -692,11 +696,9 @@ async fn git_push_meta_host( } }; match crate::github_app::resolve_clone_credentials(creds, &owner, &repo).await { - Ok((_, Some(token))) => https_url.replacen( - "https://", - &format!("https://x-access-token:{token}@"), - 1, - ), + Ok((_, Some(token))) => { + https_url.replacen("https://", &format!("https://x-access-token:{token}@"), 1) + } Ok(_) => { tracing::warn!("No token returned for metadata push"); return; @@ -915,11 +917,7 @@ impl WorkflowRunEngine { /// Run lifecycle hooks and return the merged decision. /// Returns `Proceed` if no hook runner is configured. - async fn run_hooks( - &self, - hook_context: &HookContext, - work_dir: Option<&Path>, - ) -> HookDecision { + async fn run_hooks(&self, hook_context: &HookContext, work_dir: Option<&Path>) -> HookDecision { let Some(ref runner) = self.services.hook_runner else { return HookDecision::Proceed; }; @@ -1251,11 +1249,8 @@ impl WorkflowRunEngine { // RunStart hook (blocking — can prevent run) { - let hook_ctx = HookContext::new( - HookEvent::RunStart, - run_id.clone(), - graph.name.clone(), - ); + let hook_ctx = + HookContext::new(HookEvent::RunStart, run_id.clone(), graph.name.clone()); let decision = self.run_hooks(&hook_ctx, hook_work_dir.as_deref()).await; if let HookDecision::Block { reason } = decision { let msg = reason.unwrap_or_else(|| "blocked by RunStart hook".into()); @@ -1457,17 +1452,15 @@ impl WorkflowRunEngine { if is_terminal(node) { match check_goal_gates(graph, &node_outcomes) { Ok(()) => { - self.services - .emitter - .emit(&WorkflowRunEvent::StageStarted { - node_id: node.id.clone(), - name: node.label().to_string(), - index: stage_index, - handler_type: node.handler_type().map(String::from), - script: node_script(node), - attempt: 1, - max_attempts: 1, - }); + self.services.emitter.emit(&WorkflowRunEvent::StageStarted { + node_id: node.id.clone(), + name: node.label().to_string(), + index: stage_index, + handler_type: node.handler_type().map(String::from), + script: node_script(node), + attempt: 1, + max_attempts: 1, + }); self.services .emitter .emit(&WorkflowRunEvent::StageCompleted { @@ -1504,7 +1497,13 @@ impl WorkflowRunEngine { git_commit_sha: last_git_sha.clone(), }); - self.run_failed_hook(&run_id, &graph.name, &error, hook_work_dir.as_deref()).await; + self.run_failed_hook( + &run_id, + &graph.name, + &error, + hook_work_dir.as_deref(), + ) + .await; return Ok((error.to_fail_outcome(), context)); } @@ -1557,7 +1556,10 @@ impl WorkflowRunEngine { // Step 2: Execute node handler with retry policy let visit = *loop_state.node_visits.get(¤t_node_id).unwrap_or(&1); - context.set(context::keys::INTERNAL_NODE_VISIT_COUNT, serde_json::json!(visit)); + context.set( + context::keys::INTERNAL_NODE_VISIT_COUNT, + serde_json::json!(visit), + ); context.set(context::keys::CURRENT_NODE, serde_json::json!(&node.id)); let retry_policy = build_retry_policy(node, graph); @@ -1573,28 +1575,21 @@ impl WorkflowRunEngine { // StageStart hook (blocking — can skip node) { - let mut hook_ctx = HookContext::new( - HookEvent::StageStart, - run_id.clone(), - graph.name.clone(), - ); + let mut hook_ctx = + HookContext::new(HookEvent::StageStart, run_id.clone(), graph.name.clone()); hook_ctx.cwd = hook_work_dir.as_ref().map(|p| p.display().to_string()); hook_ctx.node_id = Some(node.id.clone()); hook_ctx.node_label = Some(node.label().to_string()); hook_ctx.handler_type = node.handler_type().map(String::from); hook_ctx.attempt = Some(1); - hook_ctx.max_attempts = Some( - usize::try_from(retry_policy.max_attempts).unwrap_or(usize::MAX), - ); - let decision = self - .run_hooks(&hook_ctx, hook_work_dir.as_deref()) - .await; + hook_ctx.max_attempts = + Some(usize::try_from(retry_policy.max_attempts).unwrap_or(usize::MAX)); + let decision = self.run_hooks(&hook_ctx, hook_work_dir.as_deref()).await; match decision { HookDecision::Skip { reason } => { let mut outcome = Outcome::skipped(); - outcome.notes = Some( - reason.unwrap_or_else(|| "skipped by StageStart hook".into()), - ); + outcome.notes = + Some(reason.unwrap_or_else(|| "skipped by StageStart hook".into())); completed_nodes.push(node.id.clone()); node_outcomes.insert(node.id.clone(), outcome); previous_node_id = Some(node.id.clone()); @@ -1719,9 +1714,7 @@ impl WorkflowRunEngine { hook_ctx.handler_type = node.handler_type().map(String::from); hook_ctx.status = Some("fail".into()); hook_ctx.failure_reason = outcome.failure_reason().map(String::from); - let _ = self - .run_hooks(&hook_ctx, hook_work_dir.as_deref()) - .await; + let _ = self.run_hooks(&hook_ctx, hook_work_dir.as_deref()).await; } } else { self.services @@ -1754,9 +1747,7 @@ impl WorkflowRunEngine { hook_ctx.node_label = Some(node.label().to_string()); hook_ctx.handler_type = node.handler_type().map(String::from); hook_ctx.status = Some(outcome.status.to_string()); - let _ = self - .run_hooks(&hook_ctx, hook_work_dir.as_deref()) - .await; + let _ = self.run_hooks(&hook_ctx, hook_work_dir.as_deref()).await; } } @@ -1783,7 +1774,10 @@ impl WorkflowRunEngine { // Step 4: Apply context updates from outcome context.apply_updates(&outcome.context_updates); - context.set(context::keys::OUTCOME, serde_json::json!(outcome.status.to_string())); + context.set( + context::keys::OUTCOME, + serde_json::json!(outcome.status.to_string()), + ); context.set( context::keys::FAILURE_CLASS, serde_json::json!(outcome_failure_class.map_or(String::new(), |fc| fc.to_string())), @@ -1836,17 +1830,19 @@ impl WorkflowRunEngine { ); hook_ctx.edge_from = Some(node.id.clone()); hook_ctx.edge_to = Some(to.clone()); - hook_ctx.edge_label = next_edge.as_ref().and_then(|e| e.label().map(String::from)); - let decision = self - .run_hooks(&hook_ctx, hook_work_dir.as_deref()) - .await; + hook_ctx.edge_label = + next_edge.as_ref().and_then(|e| e.label().map(String::from)); + let decision = self.run_hooks(&hook_ctx, hook_work_dir.as_deref()).await; match decision { - HookDecision::Override { edge_to: new_target } => { + HookDecision::Override { + edge_to: new_target, + } => { // Redirect routing to the hook-specified target (None, Some(new_target)) } HookDecision::Block { reason } => { - let msg = reason.unwrap_or_else(|| "blocked by EdgeSelected hook".into()); + let msg = + reason.unwrap_or_else(|| "blocked by EdgeSelected hook".into()); return Err(ArcError::engine(msg)); } _ => (next_edge, jump_target), @@ -1897,139 +1893,144 @@ impl WorkflowRunEngine { // Step 6b: Write shadow branch first, then run branch commit with trailer // Skip git checkpoint for the start node — it's a no-op, so the commit is always empty. if start_node_id.as_deref() != Some(&*node.id) { - if let Some(ref mode) = config.git_checkpoint { - // Shadow commit (best-effort): extract repo path from either variant - let shadow_sha: Option = if config.meta_branch.is_some() { - let repo_path = match mode { - GitCheckpointMode::Host(ref p) | GitCheckpointMode::Remote(ref p) => p, - }; - let store = crate::git::MetadataStore::new(repo_path, &config.git_author); - serde_json::to_vec_pretty(&checkpoint) - .ok() - .and_then(|cp_json| { - let artifact_entries: Vec<(String, Vec)> = artifact_store - .list() - .iter() - .filter_map(|info| { - info.file_path.as_ref().and_then(|path| { - std::fs::read(path).ok().map(|data| { - (format!("artifacts/{}.json", info.id), data) + if let Some(ref mode) = config.git_checkpoint { + // Shadow commit (best-effort): extract repo path from either variant + let shadow_sha: Option = if config.meta_branch.is_some() { + let repo_path = match mode { + GitCheckpointMode::Host(ref p) | GitCheckpointMode::Remote(ref p) => p, + }; + let store = crate::git::MetadataStore::new(repo_path, &config.git_author); + serde_json::to_vec_pretty(&checkpoint) + .ok() + .and_then(|cp_json| { + let artifact_entries: Vec<(String, Vec)> = artifact_store + .list() + .iter() + .filter_map(|info| { + info.file_path.as_ref().and_then(|path| { + std::fs::read(path).ok().map(|data| { + (format!("artifacts/{}.json", info.id), data) + }) }) }) - }) - .collect(); - let artifact_refs: Vec<(&str, &[u8])> = artifact_entries - .iter() - .map(|(k, v)| (k.as_str(), v.as_slice())) - .collect(); - match store.write_checkpoint(&config.run_id, &cp_json, &artifact_refs) { - Ok(sha) => Some(sha), - Err(e) => { - context.append_log(format!( - "metadata checkpoint write failed: {e}" - )); - None + .collect(); + let artifact_refs: Vec<(&str, &[u8])> = artifact_entries + .iter() + .map(|(k, v)| (k.as_str(), v.as_slice())) + .collect(); + match store.write_checkpoint( + &config.run_id, + &cp_json, + &artifact_refs, + ) { + Ok(sha) => Some(sha), + Err(e) => { + context.append_log(format!( + "metadata checkpoint write failed: {e}" + )); + None + } } - } - }) - } else { - None - }; + }) + } else { + None + }; - // Run branch commit with Arc-Meta trailer pointing to shadow commit - let rid = run_id.clone(); - let nid = node.id.clone(); - let status_str = outcome.status.to_string(); - let completed_count = completed_nodes.len(); + // Run branch commit with Arc-Meta trailer pointing to shadow commit + let rid = run_id.clone(); + let nid = node.id.clone(); + let status_str = outcome.status.to_string(); + let completed_count = completed_nodes.len(); - let commit_result = match mode { - GitCheckpointMode::Host(work_dir) => { - git_checkpoint_host( - work_dir.clone(), - rid, - nid, - status_str, - completed_count, - shadow_sha, - config.checkpoint_exclude_globs.clone(), - config.git_author.clone(), - ) - .await - } - GitCheckpointMode::Remote(_) => { - git_checkpoint_remote( - &*self.services.sandbox, - &run_id, - &node.id, - &outcome.status.to_string(), - completed_count, - shadow_sha, - &config.checkpoint_exclude_globs, - &config.git_author, - ) - .await - } - }; - - if let Some(sha) = commit_result { - checkpoint.git_commit_sha = Some(sha.clone()); - if let Err(e) = checkpoint.save(&checkpoint_path) { - context.append_log(format!("checkpoint re-save with SHA failed: {e}")); - } - self.services - .emitter - .emit(&WorkflowRunEvent::GitCheckpoint { - run_id: run_id.clone(), - node_id: node.id.clone(), - status: outcome.status.to_string(), - git_commit_sha: sha.clone(), - }); - - // Push run branch and metadata branch to origin after remote checkpoint - if let GitCheckpointMode::Remote(ref host_repo) = mode { - if let Some(ref branch) = config.run_branch { - git_push_remote(&*self.services.sandbox, branch).await; - } - if let Some(ref meta_branch) = config.meta_branch { - git_push_meta_host( - host_repo.clone(), - meta_branch.clone(), - config.github_app.clone(), - ) - .await; - } - } - - // Save diff.patch for this stage - let prev = last_git_sha - .as_deref() - .or(config.base_sha.as_deref()) - .unwrap_or(&sha); - let diff_base = prev.to_string(); - let diff_dest = node_dir(&config.logs_root, &node.id, visit).join("diff.patch"); - - let diff_result = match mode { + let commit_result = match mode { GitCheckpointMode::Host(work_dir) => { - git_diff_host(work_dir.clone(), diff_base).await + git_checkpoint_host( + work_dir.clone(), + rid, + nid, + status_str, + completed_count, + shadow_sha, + config.checkpoint_exclude_globs.clone(), + config.git_author.clone(), + ) + .await } GitCheckpointMode::Remote(_) => { - git_diff_remote(&*self.services.sandbox, &diff_base).await + git_checkpoint_remote( + &*self.services.sandbox, + &run_id, + &node.id, + &outcome.status.to_string(), + completed_count, + shadow_sha, + &config.checkpoint_exclude_globs, + &config.git_author, + ) + .await } }; - if let Some(patch) = diff_result { - if !patch.is_empty() { - let _ = std::fs::write(&diff_dest, patch); - } - } else { - context.append_log("git diff failed".to_string()); - } - last_git_sha = Some(sha); - } else { - context.append_log("git checkpoint commit failed".to_string()); + if let Some(sha) = commit_result { + checkpoint.git_commit_sha = Some(sha.clone()); + if let Err(e) = checkpoint.save(&checkpoint_path) { + context.append_log(format!("checkpoint re-save with SHA failed: {e}")); + } + self.services + .emitter + .emit(&WorkflowRunEvent::GitCheckpoint { + run_id: run_id.clone(), + node_id: node.id.clone(), + status: outcome.status.to_string(), + git_commit_sha: sha.clone(), + }); + + // Push run branch and metadata branch to origin after remote checkpoint + if let GitCheckpointMode::Remote(ref host_repo) = mode { + if let Some(ref branch) = config.run_branch { + git_push_remote(&*self.services.sandbox, branch).await; + } + if let Some(ref meta_branch) = config.meta_branch { + git_push_meta_host( + host_repo.clone(), + meta_branch.clone(), + config.github_app.clone(), + ) + .await; + } + } + + // Save diff.patch for this stage + let prev = last_git_sha + .as_deref() + .or(config.base_sha.as_deref()) + .unwrap_or(&sha); + let diff_base = prev.to_string(); + let diff_dest = + node_dir(&config.logs_root, &node.id, visit).join("diff.patch"); + + let diff_result = match mode { + GitCheckpointMode::Host(work_dir) => { + git_diff_host(work_dir.clone(), diff_base).await + } + GitCheckpointMode::Remote(_) => { + git_diff_remote(&*self.services.sandbox, &diff_base).await + } + }; + if let Some(patch) = diff_result { + if !patch.is_empty() { + let _ = std::fs::write(&diff_dest, patch); + } + } else { + context.append_log("git diff failed".to_string()); + } + + last_git_sha = Some(sha); + } else { + context.append_log("git checkpoint commit failed".to_string()); + } } } - } // Step 7: Follow selected edge (or direct jump) if let Some(target) = jump_target { @@ -2059,7 +2060,13 @@ impl WorkflowRunEngine { git_commit_sha: last_git_sha.clone(), }); - self.run_failed_hook(&run_id, &graph.name, &error, hook_work_dir.as_deref()).await; + self.run_failed_hook( + &run_id, + &graph.name, + &error, + hook_work_dir.as_deref(), + ) + .await; return Err(error); } @@ -2141,11 +2148,8 @@ impl WorkflowRunEngine { // RunComplete hook (non-blocking) { - let hook_ctx = HookContext::new( - HookEvent::RunComplete, - run_id.clone(), - graph.name.clone(), - ); + let hook_ctx = + HookContext::new(HookEvent::RunComplete, run_id.clone(), graph.name.clone()); let _ = self.run_hooks(&hook_ctx, hook_work_dir.as_deref()).await; } @@ -3163,8 +3167,7 @@ mod tests { }; engine.run(&g, &config).await.unwrap(); - let manifest = - crate::manifest::Manifest::load(&dir.path().join("manifest.json")).unwrap(); + let manifest = crate::manifest::Manifest::load(&dir.path().join("manifest.json")).unwrap(); assert_eq!(manifest.labels.get("env").map(String::as_str), Some("test")); } @@ -3190,8 +3193,7 @@ mod tests { }; engine.run(&g, &config).await.unwrap(); - let manifest = - crate::manifest::Manifest::load(&dir.path().join("manifest.json")).unwrap(); + let manifest = crate::manifest::Manifest::load(&dir.path().join("manifest.json")).unwrap(); assert!(manifest.labels.is_empty()); } @@ -4836,7 +4838,10 @@ mod tests { // Check the checkpoint for the failure_signature context value let checkpoint_path = dir.path().join("checkpoint.json"); let cp = Checkpoint::load(&checkpoint_path).unwrap(); - let sig_value = cp.context_values.get(context::keys::FAILURE_SIGNATURE).unwrap(); + let sig_value = cp + .context_values + .get(context::keys::FAILURE_SIGNATURE) + .unwrap(); let sig_str = sig_value.as_str().unwrap(); assert!( sig_str.contains("work|deterministic|"), @@ -4855,7 +4860,16 @@ mod tests { .output() .unwrap(); std::process::Command::new("git") - .args(["-c", "user.name=Test", "-c", "user.email=test@test.com", "commit", "--allow-empty", "-m", "initial"]) + .args([ + "-c", + "user.name=Test", + "-c", + "user.email=test@test.com", + "commit", + "--allow-empty", + "-m", + "initial", + ]) .current_dir(repo) .output() .unwrap(); diff --git a/crates/arc-workflows/src/event.rs b/crates/arc-workflows/src/event.rs index b331b8257..c3453b113 100644 --- a/crates/arc-workflows/src/event.rs +++ b/crates/arc-workflows/src/event.rs @@ -520,13 +520,7 @@ impl WorkflowRunEvent { error, duration_ms, } => { - error!( - cli_name, - provider, - error, - duration_ms, - "CLI ensure failed" - ); + error!(cli_name, provider, error, duration_ms, "CLI ensure failed"); } } } diff --git a/crates/arc-workflows/src/git.rs b/crates/arc-workflows/src/git.rs index 49bd922f1..3234813f6 100644 --- a/crates/arc-workflows/src/git.rs +++ b/crates/arc-workflows/src/git.rs @@ -36,7 +36,6 @@ impl GitAuthor { } } - fn git_error(msg: impl Into) -> ArcError { ArcError::engine(msg.into()) } @@ -413,7 +412,10 @@ impl MetadataStore { } /// Read the manifest from the metadata branch. Returns `None` if not found. - pub fn read_manifest(repo_path: &Path, run_id: &str) -> Result> { + pub fn read_manifest( + repo_path: &Path, + run_id: &str, + ) -> Result> { match Self::read_file(repo_path, run_id, "manifest.json")? { Some(bytes) => { let manifest: crate::manifest::Manifest = serde_json::from_slice(&bytes) @@ -563,7 +565,17 @@ mod tests { let wt = dir.path().join("ff-wt"); add_worktree(dir.path(), &wt, "ff-branch").unwrap(); fs::write(wt.join("new.txt"), "data").unwrap(); - checkpoint_commit(&wt, "run", "node", "ok", 1, None, &[], &GitAuthor::default()).unwrap(); + checkpoint_commit( + &wt, + "run", + "node", + "ok", + 1, + None, + &[], + &GitAuthor::default(), + ) + .unwrap(); let advanced_sha = head_sha(&wt).unwrap(); remove_worktree(dir.path(), &wt).unwrap(); @@ -634,8 +646,17 @@ mod tests { // Simulate a shadow commit SHA let shadow_sha = "abcdef1234567890abcdef1234567890abcdef12"; - let sha = - checkpoint_commit(&wt_path, "run1", "nodeA", "success", 3, Some(shadow_sha), &[], &GitAuthor::default()).unwrap(); + let sha = checkpoint_commit( + &wt_path, + "run1", + "nodeA", + "success", + 3, + Some(shadow_sha), + &[], + &GitAuthor::default(), + ) + .unwrap(); assert_eq!(sha.len(), 40); assert!(sha.chars().all(|c| c.is_ascii_hexdigit())); @@ -674,7 +695,17 @@ mod tests { let wt_path = dir.path().join("worktree"); add_worktree(dir.path(), &wt_path, "run-branch2").unwrap(); - let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 1, None, &[], &GitAuthor::default()).unwrap(); + let sha = checkpoint_commit( + &wt_path, + "run2", + "nodeB", + "completed", + 1, + None, + &[], + &GitAuthor::default(), + ) + .unwrap(); assert_eq!(sha.len(), 40); // Verify Arc-Completed trailer present but no Arc-Meta @@ -718,7 +749,17 @@ mod tests { let wt_path = dir.path().join("worktree"); add_worktree(dir.path(), &wt_path, "fallback-branch").unwrap(); - let sha = checkpoint_commit(&wt_path, "run2", "nodeB", "completed", 0, None, &[], &GitAuthor::default()).unwrap(); + let sha = checkpoint_commit( + &wt_path, + "run2", + "nodeB", + "completed", + 0, + None, + &[], + &GitAuthor::default(), + ) + .unwrap(); assert_eq!(sha.len(), 40); remove_worktree(dir.path(), &wt_path).unwrap(); @@ -1003,7 +1044,17 @@ mod tests { fs::write(wt_path.join("node_modules/pkg/index.js"), "module").unwrap(); let excludes = vec!["**/node_modules/**".to_string()]; - checkpoint_commit(&wt_path, "run", "node", "ok", 1, None, &excludes, &GitAuthor::default()).unwrap(); + checkpoint_commit( + &wt_path, + "run", + "node", + "ok", + 1, + None, + &excludes, + &GitAuthor::default(), + ) + .unwrap(); // Verify kept.txt was committed let output = Command::new("git") @@ -1036,14 +1087,34 @@ mod tests { // Create and commit a file in the excluded dir first fs::create_dir_all(wt_path.join(".cache")).unwrap(); fs::write(wt_path.join(".cache/data.bin"), "v1").unwrap(); - checkpoint_commit(&wt_path, "run", "setup", "ok", 0, None, &[], &GitAuthor::default()).unwrap(); + checkpoint_commit( + &wt_path, + "run", + "setup", + "ok", + 0, + None, + &[], + &GitAuthor::default(), + ) + .unwrap(); // Now modify the tracked excluded file and add a new non-excluded file fs::write(wt_path.join(".cache/data.bin"), "v2").unwrap(); fs::write(wt_path.join("result.txt"), "done").unwrap(); let excludes = vec!["**/.cache/**".to_string()]; - checkpoint_commit(&wt_path, "run", "step", "ok", 1, None, &excludes, &GitAuthor::default()).unwrap(); + checkpoint_commit( + &wt_path, + "run", + "step", + "ok", + 1, + None, + &excludes, + &GitAuthor::default(), + ) + .unwrap(); let output = Command::new("git") .args(["show", "--name-only", "--format=", "HEAD"]) @@ -1090,9 +1161,14 @@ mod tests { .unwrap(); Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "--allow-empty", "-m", "init", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "--allow-empty", + "-m", + "init", ]) .current_dir(&repo_dir) .output() @@ -1110,7 +1186,10 @@ mod tests { .output() .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("test-push"), "remote should have test-push branch"); + assert!( + stdout.contains("test-push"), + "remote should have test-push branch" + ); } #[test] @@ -1126,9 +1205,13 @@ mod tests { .unwrap(); Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "-m", "add file", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "-m", + "add file", ]) .current_dir(dir.path()) .output() diff --git a/crates/arc-workflows/src/github_app.rs b/crates/arc-workflows/src/github_app.rs index 379534a6a..2329f8570 100644 --- a/crates/arc-workflows/src/github_app.rs +++ b/crates/arc-workflows/src/github_app.rs @@ -107,18 +107,14 @@ pub async fn create_installation_access_token( )); } 403 => { - return Err( - "GitHub App installation is suspended. \ + return Err("GitHub App installation is suspended. \ Re-enable it in your organization's GitHub App settings." - .to_string(), - ); + .to_string()); } 401 => { - return Err( - "GitHub App authentication failed. \ + return Err("GitHub App authentication failed. \ Check that app_id and GITHUB_APP_PRIVATE_KEY are correct." - .to_string(), - ); + .to_string()); } _ => { return Err(format!( @@ -162,11 +158,9 @@ pub async fn create_installation_access_token( )); } 401 => { - return Err( - "GitHub App authentication failed. \ + return Err("GitHub App authentication failed. \ Check that app_id and GITHUB_APP_PRIVATE_KEY are correct." - .to_string(), - ); + .to_string()); } _ => { return Err(format!( @@ -217,10 +211,7 @@ pub async fn resolve_clone_credentials( let token = create_installation_access_token(&client, &jwt, owner, repo, GITHUB_API_BASE_URL).await?; - Ok(( - Some("x-access-token".to_string()), - Some(token), - )) + Ok((Some("x-access-token".to_string()), Some(token))) } #[cfg(test)] @@ -307,7 +298,13 @@ mod tests { fn test_rsa_key() -> String { use std::process::Command; let output = Command::new("openssl") - .args(["genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048"]) + .args([ + "genpkey", + "-algorithm", + "RSA", + "-pkeyopt", + "rsa_keygen_bits:2048", + ]) .output() .expect("openssl should be available"); assert!(output.status.success(), "openssl keygen failed"); @@ -326,9 +323,11 @@ mod tests { let pem = test_rsa_key(); let jwt = sign_app_jwt("12345", &pem).unwrap(); let header_b64 = jwt.split('.').next().unwrap(); - let header_json = - base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, header_b64) - .unwrap(); + let header_json = base64::Engine::decode( + &base64::engine::general_purpose::URL_SAFE_NO_PAD, + header_b64, + ) + .unwrap(); let header: serde_json::Value = serde_json::from_slice(&header_json).unwrap(); assert_eq!(header["alg"], "RS256"); } @@ -338,9 +337,11 @@ mod tests { let pem = test_rsa_key(); let jwt = sign_app_jwt("99999", &pem).unwrap(); let payload_b64 = jwt.split('.').nth(1).unwrap(); - let payload_json = - base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, payload_b64) - .unwrap(); + let payload_json = base64::Engine::decode( + &base64::engine::general_purpose::URL_SAFE_NO_PAD, + payload_b64, + ) + .unwrap(); let claims: serde_json::Value = serde_json::from_slice(&payload_json).unwrap(); assert_eq!(claims["iss"], "99999"); diff --git a/crates/arc-workflows/src/graph/types.rs b/crates/arc-workflows/src/graph/types.rs index e6230aa57..893c87e22 100644 --- a/crates/arc-workflows/src/graph/types.rs +++ b/crates/arc-workflows/src/graph/types.rs @@ -70,7 +70,10 @@ impl AttrValue { /// Returns true if the handler type is an LLM-based handler (agent or prompt, including legacy aliases). #[must_use] pub fn is_llm_handler_type(handler_type: Option<&str>) -> bool { - matches!(handler_type, Some("agent") | Some("agent_loop") | Some("prompt") | Some("one_shot")) + matches!( + handler_type, + Some("agent") | Some("agent_loop") | Some("prompt") | Some("one_shot") + ) } /// Maps Graphviz shapes to handler type strings (Section 2.8). @@ -574,10 +577,8 @@ mod tests { #[test] fn node_handler_type_explicit() { let mut node = Node::new("gate"); - node.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + node.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); assert_eq!(node.handler_type(), Some("human")); } diff --git a/crates/arc-workflows/src/handler/agent.rs b/crates/arc-workflows/src/handler/agent.rs index a83d0115e..209228ce5 100644 --- a/crates/arc-workflows/src/handler/agent.rs +++ b/crates/arc-workflows/src/handler/agent.rs @@ -73,7 +73,8 @@ impl AgentHandler { /// `$gaol` at runtime. pub(crate) fn expand_variables(text: &str, graph: &Graph) -> Result { let vars = HashMap::from([("goal".to_string(), graph.goal().to_string())]); - crate::cli::run_config::expand_vars(text, &vars).map_err(|e| ArcError::Validation(e.to_string())) + crate::cli::run_config::expand_vars(text, &vars) + .map_err(|e| ArcError::Validation(e.to_string())) } /// Status fields that indicate a JSON object contains routing directives. @@ -143,7 +144,9 @@ pub(crate) fn extract_status_fields(text: &str, outcome: &mut Outcome) -> bool { }); let Some(value) = parsed else { return false }; - let Some(obj) = value.as_object() else { return false }; + let Some(obj) = value.as_object() else { + return false; + }; if let Some(label) = obj.get("preferred_next_label").and_then(|v| v.as_str()) { outcome.preferred_label = Some(label.to_string()); diff --git a/crates/arc-workflows/src/handler/human.rs b/crates/arc-workflows/src/handler/human.rs index 011660caf..c6915174a 100644 --- a/crates/arc-workflows/src/handler/human.rs +++ b/crates/arc-workflows/src/handler/human.rs @@ -4,7 +4,6 @@ use std::time::Instant; use async_trait::async_trait; -use crate::millis_u64; use crate::context::keys; use crate::context::Context; use crate::error::ArcError; @@ -13,6 +12,7 @@ use crate::graph::{Graph, Node}; use crate::interviewer::{ Answer, AnswerValue, Interviewer, Question, QuestionOption, QuestionType, }; +use crate::millis_u64; use crate::outcome::Outcome; use super::{EngineServices, Handler}; @@ -226,9 +226,10 @@ fn make_choice_outcome(key: &str, label: &str, to: &str) -> Outcome { let mut outcome = Outcome::success(); outcome.preferred_label = Some(label.to_string()); outcome.suggested_next_ids = vec![to.to_string()]; - outcome - .context_updates - .insert(keys::HUMAN_GATE_SELECTED.to_string(), serde_json::json!(key)); + outcome.context_updates.insert( + keys::HUMAN_GATE_SELECTED.to_string(), + serde_json::json!(key), + ); outcome .context_updates .insert(keys::HUMAN_GATE_LABEL.to_string(), serde_json::json!(label)); diff --git a/crates/arc-workflows/src/handler/manager_loop.rs b/crates/arc-workflows/src/handler/manager_loop.rs index 3f8245e55..27a587833 100644 --- a/crates/arc-workflows/src/handler/manager_loop.rs +++ b/crates/arc-workflows/src/handler/manager_loop.rs @@ -150,7 +150,10 @@ impl Handler for SubWorkflowHandler { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, - git_author: git_state.as_ref().map(|gs| gs.git_author.clone()).unwrap_or_default(), + git_author: git_state + .as_ref() + .map(|gs| gs.git_author.clone()) + .unwrap_or_default(), }; // Clone parent context for child; inject parent preamble @@ -683,14 +686,8 @@ mod tests { serde_json::json!("exit"), ); after.insert("current_node".to_string(), serde_json::json!("exit")); - after.insert( - "response.plan".to_string(), - serde_json::json!("the plan"), - ); - after.insert( - "review.result".to_string(), - serde_json::json!("approved"), - ); + after.insert("response.plan".to_string(), serde_json::json!("the plan")); + after.insert("review.result".to_string(), serde_json::json!("approved")); let raw_diff = context_diff(&before, &after); let filtered: HashMap = raw_diff @@ -803,8 +800,7 @@ mod tests { _logs_root: &Path, _services: &EngineServices, ) -> Result { - let parent_preamble = - context.get_string(keys::INTERNAL_PARENT_PREAMBLE, ""); + let parent_preamble = context.get_string(keys::INTERNAL_PARENT_PREAMBLE, ""); let mut outcome = Outcome::success(); outcome.context_updates.insert( "echo.parent_preamble".to_string(), diff --git a/crates/arc-workflows/src/handler/mod.rs b/crates/arc-workflows/src/handler/mod.rs index bde39aa91..2d2d647fe 100644 --- a/crates/arc-workflows/src/handler/mod.rs +++ b/crates/arc-workflows/src/handler/mod.rs @@ -123,14 +123,10 @@ pub fn default_registry( interviewer: Arc, make_backend: impl Fn() -> Option>, ) -> HandlerRegistry { - let mut registry = - HandlerRegistry::new(Box::new(agent::AgentHandler::new(make_backend()))); + let mut registry = HandlerRegistry::new(Box::new(agent::AgentHandler::new(make_backend()))); registry.register("start", Box::new(start::StartHandler)); registry.register("exit", Box::new(exit::ExitHandler)); - registry.register( - "agent", - Box::new(agent::AgentHandler::new(make_backend())), - ); + registry.register("agent", Box::new(agent::AgentHandler::new(make_backend()))); // Legacy alias registry.register( "agent_loop", @@ -146,10 +142,7 @@ pub fn default_registry( Box::new(prompt::PromptHandler::new(make_backend())), ); registry.register("conditional", Box::new(conditional::ConditionalHandler)); - registry.register( - "human", - Box::new(human::HumanHandler::new(interviewer)), - ); + registry.register("human", Box::new(human::HumanHandler::new(interviewer))); registry.register("command", Box::new(command::CommandHandler)); registry.register("tool", Box::new(command::CommandHandler)); registry.register("parallel", Box::new(parallel::ParallelHandler)); @@ -201,10 +194,8 @@ mod tests { ); let mut node = Node::new("gate"); - node.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + node.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); let handler = registry.resolve(&node); // We can verify it returns the right handler by checking it doesn't panic // and returns a valid reference diff --git a/crates/arc-workflows/src/handler/parallel.rs b/crates/arc-workflows/src/handler/parallel.rs index 9a90d0fea..c11cb4a4a 100644 --- a/crates/arc-workflows/src/handler/parallel.rs +++ b/crates/arc-workflows/src/handler/parallel.rs @@ -6,13 +6,13 @@ use arc_agent::Sandbox; use async_trait::async_trait; use tokio::sync::Semaphore; -use crate::millis_u64; use crate::context::keys; use crate::context::Context; use crate::engine::GitCheckpointMode; use crate::error::ArcError; use crate::event::WorkflowRunEvent; use crate::graph::{Graph, Node}; +use crate::millis_u64; use crate::outcome::{Outcome, StageStatus}; use super::{EngineServices, Handler}; @@ -367,7 +367,8 @@ impl Handler for ParallelHandler { "failed to reset remote worktree {wt_path_str}" ))); } - branch_context.set(keys::INTERNAL_WORK_DIR, serde_json::json!(&wt_path_str)); + branch_context + .set(keys::INTERNAL_WORK_DIR, serde_json::json!(&wt_path_str)); let env: Arc = Arc::new(WorktreeSandbox { inner: Arc::clone(&services.sandbox), worktree_dir: wt_path_str.clone(), @@ -400,7 +401,10 @@ impl Handler for ParallelHandler { let sem = Arc::clone(&semaphore); let has_git = git_state.is_some(); let run_id = git_state.as_ref().map(|gs| gs.run_id.clone()); - let git_author = git_state.as_ref().map(|gs| gs.git_author.clone()).unwrap_or_default(); + let git_author = git_state + .as_ref() + .map(|gs| gs.git_author.clone()) + .unwrap_or_default(); let handle = tokio::spawn(async move { let _permit = sem diff --git a/crates/arc-workflows/src/handler/prompt.rs b/crates/arc-workflows/src/handler/prompt.rs index f02037cfb..feb98dbb5 100644 --- a/crates/arc-workflows/src/handler/prompt.rs +++ b/crates/arc-workflows/src/handler/prompt.rs @@ -57,8 +57,13 @@ impl Handler for PromptHandler { .llm_provider() .and_then(|s| s.parse::().ok()) .unwrap_or(Provider::Anthropic); - let docs = - arc_agent::discover_project_docs(&*services.sandbox, working_dir, working_dir, provider).await; + let docs = arc_agent::discover_project_docs( + &*services.sandbox, + working_dir, + working_dir, + provider, + ) + .await; tracing::debug!(node = %node.id, doc_count = docs.len(), "Project docs discovered for prompt node"); if docs.is_empty() { None @@ -279,8 +284,7 @@ mod tests { _stage_dir: &Path, ) -> Result { *self.captured_prompt.lock().unwrap() = Some(prompt.to_string()); - *self.captured_system_prompt.lock().unwrap() = - Some(system_prompt.map(String::from)); + *self.captured_system_prompt.lock().unwrap() = Some(system_prompt.map(String::from)); Ok(CodergenResult::Text { text: "classified".to_string(), usage: None, @@ -306,7 +310,10 @@ mod tests { AttrValue::String("Classify this".to_string()), ); let context = Context::new(); - context.set(keys::CURRENT_PREAMBLE, serde_json::json!("Prior output here")); + context.set( + keys::CURRENT_PREAMBLE, + serde_json::json!("Prior output here"), + ); let graph = Graph::new("test"); let tmp = TempDir::new().unwrap(); @@ -371,10 +378,8 @@ mod tests { "prompt".to_string(), AttrValue::String("Classify this".to_string()), ); - node.attrs.insert( - "project_memory".to_string(), - AttrValue::Boolean(false), - ); + node.attrs + .insert("project_memory".to_string(), AttrValue::Boolean(false)); let context = Context::new(); let graph = Graph::new("test"); let tmp = TempDir::new().unwrap(); @@ -385,6 +390,10 @@ mod tests { .unwrap(); let sys = captured_sys.lock().unwrap().clone(); - assert_eq!(sys, Some(None), "system_prompt should be None when project_memory=false"); + assert_eq!( + sys, + Some(None), + "system_prompt should be None when project_memory=false" + ); } } diff --git a/crates/arc-workflows/src/hook/config.rs b/crates/arc-workflows/src/hook/config.rs index 431015cb2..6c042e44b 100644 --- a/crates/arc-workflows/src/hook/config.rs +++ b/crates/arc-workflows/src/hook/config.rs @@ -21,7 +21,9 @@ pub enum TlsMode { #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum HookType { - Command { command: String }, + Command { + command: String, + }, Http { url: String, headers: Option>, @@ -70,15 +72,18 @@ impl HookDefinition { if let Some(ref ht) = self.hook_type { return Some(Cow::Borrowed(ht)); } - self.command - .as_ref() - .map(|cmd| Cow::Owned(HookType::Command { command: cmd.clone() })) + self.command.as_ref().map(|cmd| { + Cow::Owned(HookType::Command { + command: cmd.clone(), + }) + }) } /// Whether this hook is blocking for its event. #[must_use] pub fn is_blocking(&self) -> bool { - self.blocking.unwrap_or_else(|| self.event.is_blocking_by_default()) + self.blocking + .unwrap_or_else(|| self.event.is_blocking_by_default()) } /// Timeout duration for this hook. @@ -115,7 +120,8 @@ impl HookDefinition { format!("{event_str}:{short}") } Some(HookType::Http { ref url, .. }) => format!("{event_str}:{url}"), - Some(HookType::Prompt { ref prompt, .. }) | Some(HookType::Agent { ref prompt, .. }) => { + Some(HookType::Prompt { ref prompt, .. }) + | Some(HookType::Agent { ref prompt, .. }) => { let short = &prompt[..arc_agent::floor_char_boundary(prompt, 20)]; format!("{event_str}:{short}") } @@ -180,7 +186,9 @@ command = "./scripts/pre-check.sh" assert_eq!(hook.event, HookEvent::StageStart); assert_eq!(hook.command.as_deref(), Some("./scripts/pre-check.sh")); let resolved = hook.resolved_hook_type().unwrap(); - assert!(matches!(&*resolved, HookType::Command { command } if command == "./scripts/pre-check.sh")); + assert!( + matches!(&*resolved, HookType::Command { command } if command == "./scripts/pre-check.sh") + ); } #[test] diff --git a/crates/arc-workflows/src/hook/executor.rs b/crates/arc-workflows/src/hook/executor.rs index d9ed83ab2..b2c9f966e 100644 --- a/crates/arc-workflows/src/hook/executor.rs +++ b/crates/arc-workflows/src/hook/executor.rs @@ -190,9 +190,7 @@ impl HookExecutorImpl { .strip_prefix("```json") .or_else(|| trimmed.strip_prefix("```")) .unwrap_or(trimmed); - let inner = inner - .strip_suffix("```") - .unwrap_or(inner); + let inner = inner.strip_suffix("```").unwrap_or(inner); inner.trim() } @@ -522,22 +520,24 @@ impl HookExecutor for HookExecutorImpl { let start = Instant::now(); let decision = match definition.resolved_hook_type() { - Some(Cow::Borrowed(HookType::Command { ref command }) - | Cow::Owned(HookType::Command { ref command })) => { - Self::execute_command(definition, command, context, &sandbox, work_dir).await - } - Some(Cow::Borrowed(HookType::Http { - ref url, - ref headers, - ref allowed_env_vars, - ref tls, - }) - | Cow::Owned(HookType::Http { - ref url, - ref headers, - ref allowed_env_vars, - ref tls, - })) => { + Some( + Cow::Borrowed(HookType::Command { ref command }) + | Cow::Owned(HookType::Command { ref command }), + ) => Self::execute_command(definition, command, context, &sandbox, work_dir).await, + Some( + Cow::Borrowed(HookType::Http { + ref url, + ref headers, + ref allowed_env_vars, + ref tls, + }) + | Cow::Owned(HookType::Http { + ref url, + ref headers, + ref allowed_env_vars, + ref tls, + }), + ) => { let clients = HTTP_CLIENTS.get_or_init(HttpClientCache::new); Self::execute_http( clients.get(tls), @@ -550,26 +550,28 @@ impl HookExecutor for HookExecutorImpl { ) .await } - Some(Cow::Borrowed(HookType::Prompt { - ref prompt, - ref model, - }) - | Cow::Owned(HookType::Prompt { - ref prompt, - ref model, - })) => { - Self::execute_prompt(prompt, model, context, definition.timeout()).await - } - Some(Cow::Borrowed(HookType::Agent { - ref prompt, - ref model, - ref max_tool_rounds, - }) - | Cow::Owned(HookType::Agent { - ref prompt, - ref model, - ref max_tool_rounds, - })) => { + Some( + Cow::Borrowed(HookType::Prompt { + ref prompt, + ref model, + }) + | Cow::Owned(HookType::Prompt { + ref prompt, + ref model, + }), + ) => Self::execute_prompt(prompt, model, context, definition.timeout()).await, + Some( + Cow::Borrowed(HookType::Agent { + ref prompt, + ref model, + ref max_tool_rounds, + }) + | Cow::Owned(HookType::Agent { + ref prompt, + ref model, + ref max_tool_rounds, + }), + ) => { Self::execute_agent( prompt, model, @@ -594,7 +596,6 @@ impl HookExecutor for HookExecutorImpl { } } - #[cfg(test)] mod tests { use super::*; @@ -606,7 +607,9 @@ mod tests { } fn make_sandbox() -> Arc { - Arc::new(arc_agent::LocalSandbox::new(std::env::current_dir().unwrap())) + Arc::new(arc_agent::LocalSandbox::new( + std::env::current_dir().unwrap(), + )) } fn test_http_client() -> reqwest::Client { @@ -717,8 +720,7 @@ mod tests { #[tokio::test] async fn command_executor_host_json_decision() { let executor = HookExecutorImpl; - let def = - make_definition(r#"echo '{"decision": "skip", "reason": "test skip"}'"#); + let def = make_definition(r#"echo '{"decision": "skip", "reason": "test skip"}'"#); let ctx = make_context(); let sandbox = make_sandbox(); let result = executor.execute(&def, &ctx, sandbox, None).await; @@ -800,7 +802,9 @@ mod tests { #[test] fn parse_prompt_response_strips_code_fences() { assert_eq!( - HookExecutorImpl::parse_prompt_response("```json\n{\"ok\": false, \"reason\": \"no\"}\n```"), + HookExecutorImpl::parse_prompt_response( + "```json\n{\"ok\": false, \"reason\": \"no\"}\n```" + ), HookDecision::Block { reason: Some("no".into()) }, @@ -809,7 +813,10 @@ mod tests { #[test] fn strip_code_fences_plain() { - assert_eq!(HookExecutorImpl::strip_code_fences(r#"{"ok": true}"#), r#"{"ok": true}"#); + assert_eq!( + HookExecutorImpl::strip_code_fences(r#"{"ok": true}"#), + r#"{"ok": true}"# + ); } #[test] @@ -833,10 +840,8 @@ mod tests { #[test] fn interpolate_resolves_allowed_var() { std::env::set_var("ARC_TEST_KEY_1", "secret123"); - let result = interpolate_env_vars( - "Bearer $ARC_TEST_KEY_1", - &["ARC_TEST_KEY_1".to_string()], - ); + let result = + interpolate_env_vars("Bearer $ARC_TEST_KEY_1", &["ARC_TEST_KEY_1".to_string()]); assert_eq!(result, "Bearer secret123"); std::env::remove_var("ARC_TEST_KEY_1"); } @@ -844,10 +849,7 @@ mod tests { #[test] fn interpolate_resolves_braced_var() { std::env::set_var("ARC_TEST_KEY_2", "val"); - let result = interpolate_env_vars( - "x${ARC_TEST_KEY_2}y", - &["ARC_TEST_KEY_2".to_string()], - ); + let result = interpolate_env_vars("x${ARC_TEST_KEY_2}y", &["ARC_TEST_KEY_2".to_string()]); assert_eq!(result, "xvaly"); std::env::remove_var("ARC_TEST_KEY_2"); } @@ -855,10 +857,7 @@ mod tests { #[test] fn interpolate_unlisted_var_becomes_empty() { std::env::set_var("ARC_TEST_KEY_3", "should_not_appear"); - let result = interpolate_env_vars( - "prefix-$ARC_TEST_KEY_3-suffix", - &[], - ); + let result = interpolate_env_vars("prefix-$ARC_TEST_KEY_3-suffix", &[]); assert_eq!(result, "prefix--suffix"); std::env::remove_var("ARC_TEST_KEY_3"); } @@ -866,10 +865,8 @@ mod tests { #[test] fn interpolate_missing_var_becomes_empty() { std::env::remove_var("ARC_TEST_NOEXIST"); - let result = interpolate_env_vars( - "a$ARC_TEST_NOEXIST-b", - &["ARC_TEST_NOEXIST".to_string()], - ); + let result = + interpolate_env_vars("a$ARC_TEST_NOEXIST-b", &["ARC_TEST_NOEXIST".to_string()]); assert_eq!(result, "a-b"); } @@ -1007,9 +1004,10 @@ mod tests { .create_async() .await; - let headers = HashMap::from([ - ("Authorization".to_string(), "Bearer $ARC_TEST_TOKEN".to_string()), - ]); + let headers = HashMap::from([( + "Authorization".to_string(), + "Bearer $ARC_TEST_TOKEN".to_string(), + )]); let client = test_http_client(); let decision = HookExecutorImpl::execute_http( diff --git a/crates/arc-workflows/src/hook/runner.rs b/crates/arc-workflows/src/hook/runner.rs index ec6adff8e..1351b034a 100644 --- a/crates/arc-workflows/src/hook/runner.rs +++ b/crates/arc-workflows/src/hook/runner.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use arc_agent::Sandbox; use super::config::{HookConfig, HookDefinition}; -use super::executor::{HookExecutorImpl, HookExecutor}; +use super::executor::{HookExecutor, HookExecutorImpl}; use super::types::{HookContext, HookDecision}; /// Central orchestrator: filters matching hooks, executes them, merges decisions. @@ -234,7 +234,9 @@ mod tests { } fn make_sandbox() -> Arc { - Arc::new(arc_agent::LocalSandbox::new(std::env::current_dir().unwrap())) + Arc::new(arc_agent::LocalSandbox::new( + std::env::current_dir().unwrap(), + )) } fn make_context(event: HookEvent) -> HookContext { @@ -287,9 +289,7 @@ mod tests { async fn matcher_filters_by_node_id() { let mut hook = make_hook(HookEvent::StageStart, "filtered"); hook.matcher = Some("agent".into()); - let config = HookConfig { - hooks: vec![hook], - }; + let config = HookConfig { hooks: vec![hook] }; let runner = HookRunner::with_executor( config, Arc::new(MockExecutor { @@ -316,9 +316,7 @@ mod tests { async fn matcher_filters_by_handler_type() { let mut hook = make_hook(HookEvent::StageStart, "filtered"); hook.matcher = Some("^agent$".into()); - let config = HookConfig { - hooks: vec![hook], - }; + let config = HookConfig { hooks: vec![hook] }; let runner = HookRunner::with_executor( config, Arc::new(MockExecutor { @@ -358,9 +356,7 @@ mod tests { async fn blocking_hook_skip_decision() { let mut hook = make_hook(HookEvent::StageStart, "skipper"); hook.blocking = Some(true); - let config = HookConfig { - hooks: vec![hook], - }; + let config = HookConfig { hooks: vec![hook] }; let runner = HookRunner::with_executor( config, Arc::new(MockExecutor { @@ -379,9 +375,7 @@ mod tests { async fn non_blocking_hook_doesnt_block() { let mut hook = make_hook(HookEvent::StageComplete, "observer"); hook.blocking = Some(false); - let config = HookConfig { - hooks: vec![hook], - }; + let config = HookConfig { hooks: vec![hook] }; let runner = HookRunner::with_executor( config, Arc::new(MockExecutor { diff --git a/crates/arc-workflows/src/hook/types.rs b/crates/arc-workflows/src/hook/types.rs index 6bd99471b..a4b38e7e1 100644 --- a/crates/arc-workflows/src/hook/types.rs +++ b/crates/arc-workflows/src/hook/types.rs @@ -126,7 +126,6 @@ pub enum HookDecision { }, } - impl HookDecision { /// Merge two decisions. Block > Skip/Override > Proceed. #[must_use] @@ -237,11 +236,7 @@ mod tests { #[test] fn hook_context_omits_none_fields() { - let ctx = HookContext::new( - HookEvent::RunStart, - "run-1".into(), - "wf".into(), - ); + let ctx = HookContext::new(HookEvent::RunStart, "run-1".into(), "wf".into()); let json = serde_json::to_string(&ctx).unwrap(); assert!(!json.contains("node_id")); assert!(!json.contains("failure_reason")); @@ -303,10 +298,7 @@ mod tests { proceed.clone().merge(skip.clone()), HookDecision::Skip { .. } )); - assert!(matches!( - skip.merge(proceed), - HookDecision::Skip { .. } - )); + assert!(matches!(skip.merge(proceed), HookDecision::Skip { .. })); } #[test] @@ -318,10 +310,7 @@ mod tests { edge_to: "x".into(), }; // First non-Proceed wins when no Block - assert!(matches!( - skip.merge(override_d), - HookDecision::Skip { .. } - )); + assert!(matches!(skip.merge(override_d), HookDecision::Skip { .. })); } #[test] diff --git a/crates/arc-workflows/src/interviewer/console.rs b/crates/arc-workflows/src/interviewer/console.rs index acfaab4e1..a87a10dc4 100644 --- a/crates/arc-workflows/src/interviewer/console.rs +++ b/crates/arc-workflows/src/interviewer/console.rs @@ -114,8 +114,14 @@ fn ask_multi_select_interactive(question: &Question) -> Answer { match selection { Ok(Some(indices)) if !indices.is_empty() => { - let keys: Vec = indices.iter().map(|&i| question.options[i].key.clone()).collect(); - let options: Vec<_> = indices.iter().map(|&i| question.options[i].clone()).collect(); + let keys: Vec = indices + .iter() + .map(|&i| question.options[i].key.clone()) + .collect(); + let options: Vec<_> = indices + .iter() + .map(|&i| question.options[i].clone()) + .collect(); Answer::multi_selected(keys, options) } _ => Answer::skipped(), diff --git a/crates/arc-workflows/src/lib.rs b/crates/arc-workflows/src/lib.rs index 6affde403..2e78b5444 100644 --- a/crates/arc-workflows/src/lib.rs +++ b/crates/arc-workflows/src/lib.rs @@ -29,14 +29,15 @@ pub mod artifact; pub mod asset_snapshot; pub mod checkpoint; pub mod cli; +pub mod conclusion; pub mod condition; pub mod context; pub mod daytona_sandbox; pub mod engine; -pub mod github_app; pub mod error; pub mod event; pub mod git; +pub mod github_app; pub mod graph; pub mod handler; pub mod hook; @@ -47,7 +48,6 @@ pub mod parser; pub mod preamble; pub mod retro; pub mod retro_agent; -pub mod conclusion; pub mod stylesheet; pub mod transform; pub mod validation; diff --git a/crates/arc-workflows/src/parser/semantic.rs b/crates/arc-workflows/src/parser/semantic.rs index 93f55e275..9c129d4c6 100644 --- a/crates/arc-workflows/src/parser/semantic.rs +++ b/crates/arc-workflows/src/parser/semantic.rs @@ -592,10 +592,7 @@ mod tests { id: "gate".into(), attrs: Some(vec![ ("type".into(), AstValue::Str("human".into())), - ( - "codergen_mode".into(), - AstValue::Str("one_shot".into()), - ), + ("codergen_mode".into(), AstValue::Str("one_shot".into())), ]), })], }; diff --git a/crates/arc-workflows/src/preamble.rs b/crates/arc-workflows/src/preamble.rs index 25f80e10d..d56bbac66 100644 --- a/crates/arc-workflows/src/preamble.rs +++ b/crates/arc-workflows/src/preamble.rs @@ -68,7 +68,9 @@ pub fn build_preamble( let parent_preamble = context.get_string(keys::INTERNAL_PARENT_PREAMBLE, ""); if !parent_preamble.is_empty() && !preamble.is_empty() { - format!("## Parent workflow context\n{parent_preamble}\n\n## Current sub-workflow\n{preamble}") + format!( + "## Parent workflow context\n{parent_preamble}\n\n## Current sub-workflow\n{preamble}" + ) } else { preamble } @@ -728,9 +730,15 @@ mod tests { context.set(keys::INTERNAL_FIDELITY, serde_json::json!("compact")); context.set(&keys::retry_count_key("plan"), serde_json::json!(1)); context.set(keys::CURRENT_NODE, serde_json::json!("work")); - context.set(&keys::graph_attr_key("default_fidelity"), serde_json::json!("compact")); + context.set( + &keys::graph_attr_key("default_fidelity"), + serde_json::json!("compact"), + ); context.set("thread.main.current_node", serde_json::json!("work")); - context.set(&keys::response_key("plan"), serde_json::json!("some response")); + context.set( + &keys::response_key("plan"), + serde_json::json!("some response"), + ); context.set(keys::LAST_STAGE, serde_json::json!("plan")); context.set(keys::LAST_RESPONSE, serde_json::json!("resp")); context.set(keys::PREFERRED_LABEL, serde_json::json!("success")); @@ -916,9 +924,15 @@ mod tests { fn compact_context_excludes_engine_keys() { let graph = Graph::new("test"); let context = Context::new(); - context.set(&keys::graph_attr_key("default_fidelity"), serde_json::json!("compact")); + context.set( + &keys::graph_attr_key("default_fidelity"), + serde_json::json!("compact"), + ); context.set("thread.main.current_node", serde_json::json!("work")); - context.set(&keys::response_key("plan"), serde_json::json!("some LLM response")); + context.set( + &keys::response_key("plan"), + serde_json::json!("some LLM response"), + ); context.set(keys::LAST_STAGE, serde_json::json!("plan")); context.set("user.preference", serde_json::json!("dark")); let completed_nodes: Vec = Vec::new(); @@ -1564,10 +1578,7 @@ mod tests { preamble.contains("## Stage: report"), "should have stage heading" ); - assert!( - preamble.contains("Handler: agent"), - "should show handler" - ); + assert!(preamble.contains("Handler: agent"), "should show handler"); assert!( preamble.contains("Model: claude-sonnet-4-20250514"), "should show model" @@ -1669,9 +1680,13 @@ mod tests { assert!(is_context_key_excluded(&keys::retry_count_key("plan"))); assert!(is_context_key_excluded(keys::CURRENT_NODE)); assert!(is_context_key_excluded(keys::CURRENT_PREAMBLE)); - assert!(is_context_key_excluded(&keys::graph_attr_key("default_fidelity"))); + assert!(is_context_key_excluded(&keys::graph_attr_key( + "default_fidelity" + ))); assert!(is_context_key_excluded(keys::GRAPH_GOAL)); - assert!(is_context_key_excluded(&keys::thread_current_node_key("main"))); + assert!(is_context_key_excluded(&keys::thread_current_node_key( + "main" + ))); assert!(is_context_key_excluded(&keys::response_key("plan"))); assert!(is_context_key_excluded(keys::OUTCOME)); assert!(is_context_key_excluded(keys::LAST_STAGE)); @@ -1714,10 +1729,7 @@ mod tests { !preamble.contains("**start**"), "should not show start node, got:\n{preamble}" ); - assert!( - preamble.contains("**plan**"), - "should show non-meta nodes" - ); + assert!(preamble.contains("**plan**"), "should show non-meta nodes"); } #[test] @@ -1826,10 +1838,7 @@ mod tests { !preamble.contains("- start:"), "should not show start stage, got:\n{preamble}" ); - assert!( - preamble.contains("- work:"), - "should show non-meta stages" - ); + assert!(preamble.contains("- work:"), "should show non-meta stages"); } #[test] @@ -1862,10 +1871,7 @@ mod tests { !preamble.contains("- start:"), "should not show start stage, got:\n{preamble}" ); - assert!( - preamble.contains("- work:"), - "should show non-meta stages" - ); + assert!(preamble.contains("- work:"), "should show non-meta stages"); } #[test] diff --git a/crates/arc-workflows/src/transform.rs b/crates/arc-workflows/src/transform.rs index 39630b0d8..9a8c2e97e 100644 --- a/crates/arc-workflows/src/transform.rs +++ b/crates/arc-workflows/src/transform.rs @@ -643,18 +643,19 @@ mod tests { .unwrap(); std::process::Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "-m", "add", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "-m", + "add", ]) .current_dir(dir.path()) .output() .unwrap(); - assert_eq!( - resolve_file_ref("@tracked.md", dir.path()), - "@tracked.md" - ); + assert_eq!(resolve_file_ref("@tracked.md", dir.path()), "@tracked.md"); } #[test] @@ -668,9 +669,14 @@ mod tests { .unwrap(); std::process::Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "--allow-empty", "-m", "init", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "--allow-empty", + "-m", + "init", ]) .current_dir(dir.path()) .output() @@ -678,10 +684,7 @@ mod tests { std::fs::write(dir.path().join("local.md"), "inlined content").unwrap(); - assert_eq!( - resolve_file_ref("@local.md", dir.path()), - "inlined content" - ); + assert_eq!(resolve_file_ref("@local.md", dir.path()), "inlined content"); } // ----------------------------------------------------------------------- @@ -699,9 +702,14 @@ mod tests { .unwrap(); std::process::Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "--allow-empty", "-m", "init", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "--allow-empty", + "-m", + "init", ]) .current_dir(dir.path()) .output() @@ -756,9 +764,14 @@ mod tests { .unwrap(); std::process::Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "--allow-empty", "-m", "init", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "--allow-empty", + "-m", + "init", ]) .current_dir(dir.path()) .output() @@ -781,9 +794,14 @@ mod tests { .unwrap(); std::process::Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "--allow-empty", "-m", "init", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "--allow-empty", + "-m", + "init", ]) .current_dir(dir.path()) .output() @@ -814,9 +832,13 @@ mod tests { .unwrap(); std::process::Command::new("git") .args([ - "-c", "user.name=test", - "-c", "user.email=test@test", - "commit", "-m", "add", + "-c", + "user.name=test", + "-c", + "user.email=test@test", + "commit", + "-m", + "add", ]) .current_dir(dir.path()) .output() diff --git a/crates/arc-workflows/src/validation/rules.rs b/crates/arc-workflows/src/validation/rules.rs index d1dc4b190..6e594f4c5 100644 --- a/crates/arc-workflows/src/validation/rules.rs +++ b/crates/arc-workflows/src/validation/rules.rs @@ -624,10 +624,7 @@ impl LintRule for PromptOnLlmNodesRule { diagnostics.push(Diagnostic { rule: self.name().to_string(), severity: Severity::Warning, - message: format!( - "LLM node '{}' has no prompt or label attribute", - node.id - ), + message: format!("LLM node '{}' has no prompt or label attribute", node.id), node_id: Some(node.id.clone()), edge: None, fix: Some("Add a prompt or label attribute".to_string()), @@ -1263,10 +1260,8 @@ mod tests { fn type_known_rule_known_type() { let mut g = minimal_graph(); let mut node = Node::new("gate"); - node.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + node.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); g.nodes.insert("gate".to_string(), node); let rule = TypeKnownRule; let d = rule.apply(&g); @@ -1925,10 +1920,8 @@ mod tests { fn freeform_edge_count_rule_zero_freeform() { let mut g = minimal_graph(); let mut gate = Node::new("gate"); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); g.nodes.insert("gate".to_string(), gate); g.nodes.insert("a".to_string(), Node::new("a")); g.edges.push(Edge::new("gate", "a")); @@ -2074,10 +2067,8 @@ mod tests { let mut g = minimal_graph(); let mut n1 = Node::new("n1"); - n1.attrs.insert( - "type".to_string(), - AttrValue::String("agent".to_string()), - ); + n1.attrs + .insert("type".to_string(), AttrValue::String("agent".to_string())); g.nodes.insert("n1".to_string(), n1); let mut n2 = Node::new("n2"); @@ -2124,10 +2115,8 @@ mod tests { fn prompt_on_llm_nodes_rule_explicit_agent_type_no_prompt() { let mut g = minimal_graph(); let mut node = Node::new("work"); - node.attrs.insert( - "type".to_string(), - AttrValue::String("agent".to_string()), - ); + node.attrs + .insert("type".to_string(), AttrValue::String("agent".to_string())); // No shape=box, but explicit type=agent node.attrs.insert( "shape".to_string(), @@ -2164,10 +2153,8 @@ mod tests { fn freeform_edge_count_rule_explicit_type_two_freeform() { let mut g = minimal_graph(); let mut gate = Node::new("gate"); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); g.nodes.insert("gate".to_string(), gate); g.nodes.insert("a".to_string(), Node::new("a")); g.nodes.insert("b".to_string(), Node::new("b")); diff --git a/crates/arc-workflows/tests/daytona_integration.rs b/crates/arc-workflows/tests/daytona_integration.rs index 590ddc729..cfba4c48a 100644 --- a/crates/arc-workflows/tests/daytona_integration.rs +++ b/crates/arc-workflows/tests/daytona_integration.rs @@ -56,20 +56,18 @@ fn load_github_app_credentials() -> arc_workflows::github_app::GitHubAppCredenti app_id: Option, } - let config: Config = - toml::from_str(&config_str).expect("Failed to parse server.toml"); - let app_id = config.git.app_id.expect("app_id not set in server.toml [git] section"); + let config: Config = toml::from_str(&config_str).expect("Failed to parse server.toml"); + let app_id = config + .git + .app_id + .expect("app_id not set in server.toml [git] section"); - let raw = std::env::var("GITHUB_APP_PRIVATE_KEY") - .expect("GITHUB_APP_PRIVATE_KEY not set"); + let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").expect("GITHUB_APP_PRIVATE_KEY not set"); let private_key_pem = if raw.starts_with("-----") { raw } else { - let bytes = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - &raw, - ) - .expect("GITHUB_APP_PRIVATE_KEY is not valid base64"); + let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &raw) + .expect("GITHUB_APP_PRIVATE_KEY is not valid base64"); String::from_utf8(bytes).expect("GITHUB_APP_PRIVATE_KEY decoded to invalid UTF-8") }; arc_workflows::github_app::GitHubAppCredentials { @@ -179,9 +177,9 @@ async fn daytona_snapshot_sandbox() { cpu: Some(2), memory: Some(4), disk: Some(10), - dockerfile: Some( - arc_workflows::daytona_sandbox::DockerfileSource::Inline("FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y ripgrep".to_string()), - ), + dockerfile: Some(arc_workflows::daytona_sandbox::DockerfileSource::Inline( + "FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y ripgrep".to_string(), + )), }), ..DaytonaConfig::default() }; @@ -1286,13 +1284,10 @@ async fn daytona_clone_public_repo_gets_credentials() { let creds = load_github_app_credentials(); // Directly test resolve_clone_credentials against a known public repo - let (username, password) = arc_workflows::github_app::resolve_clone_credentials( - &creds, - "rust-lang", - "rust", - ) - .await - .unwrap(); + let (username, password) = + arc_workflows::github_app::resolve_clone_credentials(&creds, "rust-lang", "rust") + .await + .unwrap(); assert_eq!( username.as_deref(), @@ -1312,14 +1307,13 @@ async fn daytona_clone_public_repo_gets_credentials() { async fn daytona_iat_not_installed_gives_clear_error() { let creds = load_github_app_credentials(); - let result = arc_workflows::github_app::resolve_clone_credentials( - &creds, - "torvalds", - "linux", - ) - .await; + let result = + arc_workflows::github_app::resolve_clone_credentials(&creds, "torvalds", "linux").await; - assert!(result.is_err(), "should fail for repo the app isn't installed on"); + assert!( + result.is_err(), + "should fail for repo the app isn't installed on" + ); let err = result.unwrap_err(); assert!( err.contains("not installed"), @@ -1374,14 +1368,17 @@ async fn daytona_git_push_run_branch_to_origin() { ); let mut start = Node::new("start"); - start - .attrs - .insert("shape".to_string(), AttrValue::String("Mdiamond".to_string())); + start.attrs.insert( + "shape".to_string(), + AttrValue::String("Mdiamond".to_string()), + ); graph.nodes.insert("start".to_string(), start); let mut exit = Node::new("exit"); - exit.attrs - .insert("shape".to_string(), AttrValue::String("Msquare".to_string())); + exit.attrs.insert( + "shape".to_string(), + AttrValue::String("Msquare".to_string()), + ); graph.nodes.insert("exit".to_string(), exit); let mut work = Node::new("work"); @@ -1472,8 +1469,14 @@ async fn daytona_toolbox_idle_diagnostic() { let result = env .exec_command("echo alive", 30_000, None, None, None) .await; - eprintln!("[t=0s] exec_command after init: {:?}", result.as_ref().map(|r| r.exit_code)); - assert!(result.is_ok(), "exec_command should work immediately after init"); + eprintln!( + "[t=0s] exec_command after init: {:?}", + result.as_ref().map(|r| r.exit_code) + ); + assert!( + result.is_ok(), + "exec_command should work immediately after init" + ); let sandbox_name = env.sandbox_info(); eprintln!("[t=0s] sandbox: {sandbox_name}"); @@ -1489,7 +1492,11 @@ async fn daytona_toolbox_idle_diagnostic() { match &result { Ok(r) => { - eprintln!("[t=+{sleep_secs}s] OK exit_code={} stdout={}", r.exit_code, r.stdout.trim()); + eprintln!( + "[t=+{sleep_secs}s] OK exit_code={} stdout={}", + r.exit_code, + r.stdout.trim() + ); } Err(e) => { eprintln!("[t=+{sleep_secs}s] FAILED: {e}"); @@ -1525,13 +1532,18 @@ async fn daytona_toolbox_idle_diagnostic() { // Get toolbox proxy URL and try a direct call let proxy_resp = client - .get(format!("{api_url}/sandbox/{sandbox_name}/toolbox-proxy-url")) + .get(format!( + "{api_url}/sandbox/{sandbox_name}/toolbox-proxy-url" + )) .bearer_auth(&api_key) .send() .await; if let Ok(resp) = proxy_resp { let body = resp.text().await.unwrap_or_default(); - eprintln!("[diag] proxy URL response: {}", &body[..body.len().min(200)]); + eprintln!( + "[diag] proxy URL response: {}", + &body[..body.len().min(200)] + ); if let Some(url) = serde_json::from_str::(&body) .ok() .and_then(|v| v.get("url").and_then(|u| u.as_str()).map(String::from)) @@ -1548,12 +1560,16 @@ async fn daytona_toolbox_idle_diagnostic() { Ok(resp) => { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); - eprintln!("[diag] direct call: {status} body={}", &body[..body.len().min(300)]); + eprintln!( + "[diag] direct call: {status} body={}", + &body[..body.len().min(300)] + ); } Err(e) => { // Walk the FULL error source chain let mut msg = format!("[diag] direct call FAILED: {e}"); - let mut source: Option<&dyn std::error::Error> = std::error::Error::source(&e); + let mut source: Option<&dyn std::error::Error> = + std::error::Error::source(&e); while let Some(cause) = source { msg.push_str(&format!("\n caused by: {cause}")); source = cause.source(); diff --git a/crates/arc-workflows/tests/integration.rs b/crates/arc-workflows/tests/integration.rs index f7deaca77..03f103fd7 100644 --- a/crates/arc-workflows/tests/integration.rs +++ b/crates/arc-workflows/tests/integration.rs @@ -10,14 +10,14 @@ use arc_workflows::engine::{RunConfig, WorkflowRunEngine}; use arc_workflows::error::ArcError; use arc_workflows::event::{EventEmitter, WorkflowRunEvent}; use arc_workflows::graph::{AttrValue, Edge, Graph, Node}; -use arc_workflows::handler::agent::{CodergenBackend, AgentHandler, CodergenResult}; +use arc_workflows::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; +use arc_workflows::handler::command::CommandHandler; use arc_workflows::handler::conditional::ConditionalHandler; use arc_workflows::handler::default_registry; use arc_workflows::handler::exit::ExitHandler; -use arc_workflows::handler::manager_loop::SubWorkflowHandler; -use arc_workflows::handler::command::CommandHandler; -use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::human::HumanHandler; +use arc_workflows::handler::manager_loop::SubWorkflowHandler; +use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::wait::WaitHandler; use arc_workflows::handler::{Handler, HandlerRegistry}; use arc_workflows::interviewer::auto_approve::AutoApproveInterviewer; @@ -401,10 +401,8 @@ async fn end_to_end_human_gate_pipeline() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Review Changes".to_string()), @@ -1959,10 +1957,8 @@ async fn auto_approve_interviewer_e2e() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs .insert("label".to_string(), AttrValue::String("Review".to_string())); graph.nodes.insert("gate".to_string(), gate); @@ -2174,10 +2170,8 @@ async fn human_gate_loops_back() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs .insert("label".to_string(), AttrValue::String("Review".to_string())); graph.nodes.insert("gate".to_string(), gate); @@ -5675,7 +5669,7 @@ mod real_llm { use arc_workflows::context::Context; use arc_workflows::error::ArcError; use arc_workflows::graph::Node; - use arc_workflows::handler::agent::{CodergenBackend, AgentHandler, CodergenResult}; + use arc_workflows::handler::agent::{AgentHandler, CodergenBackend, CodergenResult}; use arc_llm::client::Client; use arc_llm::types::{Message, Request}; @@ -5749,8 +5743,8 @@ mod real_llm { use arc_workflows::event::EventEmitter; use arc_workflows::graph::{AttrValue, Edge, Graph}; use arc_workflows::handler::exit::ExitHandler; - use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::human::HumanHandler; + use arc_workflows::handler::start::StartHandler; use arc_workflows::handler::HandlerRegistry; use arc_workflows::interviewer::auto_approve::AutoApproveInterviewer; use arc_workflows::outcome::StageStatus; @@ -5835,7 +5829,7 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, - git_author: arc_workflows::git::GitAuthor::default(), + git_author: arc_workflows::git::GitAuthor::default(), }; let outcome = tokio::time::timeout( @@ -5950,7 +5944,7 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, - git_author: arc_workflows::git::GitAuthor::default(), + git_author: arc_workflows::git::GitAuthor::default(), }; let outcome = tokio::time::timeout( @@ -6016,10 +6010,8 @@ mod real_llm { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Approve?".to_string()), @@ -6092,7 +6084,7 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, - git_author: arc_workflows::git::GitAuthor::default(), + git_author: arc_workflows::git::GitAuthor::default(), }; let outcome = tokio::time::timeout( @@ -6200,7 +6192,7 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, - git_author: arc_workflows::git::GitAuthor::default(), + git_author: arc_workflows::git::GitAuthor::default(), }; let outcome = tokio::time::timeout( @@ -6254,10 +6246,8 @@ async fn human_gate_freeform_only_routes_text() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Enter feedback".to_string()), @@ -6360,10 +6350,8 @@ async fn human_gate_freeform_with_fixed_choice_match() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Review Changes".to_string()), @@ -6483,10 +6471,8 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Review Changes".to_string()), @@ -6620,10 +6606,8 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Pick or type".to_string()), @@ -6731,10 +6715,8 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { "shape".to_string(), AttrValue::String("hexagon".to_string()), ); - gate.attrs.insert( - "type".to_string(), - AttrValue::String("human".to_string()), - ); + gate.attrs + .insert("type".to_string(), AttrValue::String("human".to_string())); gate.attrs.insert( "label".to_string(), AttrValue::String("Pick one".to_string()), @@ -7010,9 +6992,7 @@ fn subgraph_without_label_no_class_derived() { // --------------------------------------------------------------------------- /// Helper: create a WorkflowRunEngine with hooks configured from HookDefinitions. -fn engine_with_hooks( - hooks: Vec, -) -> WorkflowRunEngine { +fn engine_with_hooks(hooks: Vec) -> WorkflowRunEngine { let registry = make_linear_registry(); let emitter = Arc::new(EventEmitter::new()); let sandbox = local_env(); @@ -7028,7 +7008,10 @@ fn engine_with_hooks( /// Helper: create a WorkflowRunEngine with hooks and event capture. fn engine_with_hooks_and_events( hooks: Vec, -) -> (WorkflowRunEngine, Arc>>) { +) -> ( + WorkflowRunEngine, + Arc>>, +) { let registry = make_linear_registry(); let mut emitter = EventEmitter::new(); let events = collect_events(&mut emitter); @@ -7151,13 +7134,17 @@ async fn hook_run_start_block_prevents_run() { // WorkflowRunStarted should still have been emitted (it fires before the hook) let captured = events.lock().unwrap(); assert!( - captured.iter().any(|e| matches!(e, WorkflowRunEvent::WorkflowRunStarted { .. })), + captured + .iter() + .any(|e| matches!(e, WorkflowRunEvent::WorkflowRunStarted { .. })), "WorkflowRunStarted should be emitted before hook blocks" ); // But no StageStarted — the run never reached node execution assert!( - !captured.iter().any(|e| matches!(e, WorkflowRunEvent::StageStarted { .. })), + !captured + .iter() + .any(|e| matches!(e, WorkflowRunEvent::StageStarted { .. })), "No stage should start when RunStart hook blocks" ); } @@ -7201,7 +7188,11 @@ async fn hook_stage_start_proceed_allows_execution() { // Work node should have executed (response.md exists) assert!( - dir.path().join("nodes").join("work").join("response.md").exists(), + dir.path() + .join("nodes") + .join("work") + .join("response.md") + .exists(), "response.md should exist when StageStart hook proceeds" ); } @@ -7224,7 +7215,11 @@ async fn hook_stage_start_skip_bypasses_node() { // response.md should NOT exist for the work node (it was skipped) assert!( - !dir.path().join("nodes").join("work").join("response.md").exists(), + !dir.path() + .join("nodes") + .join("work") + .join("response.md") + .exists(), "response.md should not exist when StageStart hook skips node" ); @@ -7232,8 +7227,10 @@ async fn hook_stage_start_skip_bypasses_node() { let captured = events.lock().unwrap(); let stage_starts: Vec<_> = captured .iter() - .filter(|e| matches!(e, WorkflowRunEvent::StageStarted { handler_type, .. } - if handler_type.as_deref() != Some("start") && handler_type.as_deref() != Some("exit"))) + .filter(|e| { + matches!(e, WorkflowRunEvent::StageStarted { handler_type, .. } + if handler_type.as_deref() != Some("start") && handler_type.as_deref() != Some("exit")) + }) .collect(); assert!( !stage_starts.is_empty(), @@ -7277,13 +7274,21 @@ async fn hook_stage_start_matcher_filters_by_node_id() { // step1 should have executed (response.md exists) assert!( - dir.path().join("nodes").join("step1").join("response.md").exists(), + dir.path() + .join("nodes") + .join("step1") + .join("response.md") + .exists(), "step1 should execute because matcher doesn't match it" ); // step2 should have been skipped (no response.md) assert!( - !dir.path().join("nodes").join("step2").join("response.md").exists(), + !dir.path() + .join("nodes") + .join("step2") + .join("response.md") + .exists(), "step2 should be skipped because matcher matches it" ); } @@ -7291,10 +7296,7 @@ async fn hook_stage_start_matcher_filters_by_node_id() { #[tokio::test] async fn hook_stage_start_matcher_no_match_proceeds() { // Hook with matcher that matches nothing - let mut hook = make_hook( - arc_workflows::hook::HookEvent::StageStart, - "exit 1", - ); + let mut hook = make_hook(arc_workflows::hook::HookEvent::StageStart, "exit 1"); hook.matcher = Some("nonexistent_node".into()); let hooks = vec![hook]; @@ -7326,7 +7328,10 @@ async fn hook_stage_complete_fires_after_success() { assert_eq!(outcome.status, StageStatus::Success); // Marker file should exist and contain node IDs - assert!(marker.exists(), "StageComplete hook should have written marker file"); + assert!( + marker.exists(), + "StageComplete hook should have written marker file" + ); let content = std::fs::read_to_string(&marker).unwrap(); // start, step1, step2, exit all complete — hook fires for each assert!( @@ -7565,10 +7570,7 @@ async fn hook_edge_selected_override_redirects_routing() { #[tokio::test] async fn hook_edge_selected_block_aborts_run() { - let mut hook = make_hook( - arc_workflows::hook::HookEvent::EdgeSelected, - "exit 1", - ); + let mut hook = make_hook(arc_workflows::hook::HookEvent::EdgeSelected, "exit 1"); hook.matcher = Some("^plan$".into()); let hooks = vec![hook]; @@ -7578,10 +7580,7 @@ async fn hook_edge_selected_block_aborts_run() { let config = make_run_config(dir.path()); let result = engine.run(&graph, &config).await; - assert!( - result.is_err(), - "EdgeSelected block should abort the run" - ); + assert!(result.is_err(), "EdgeSelected block should abort the run"); } // --- CheckpointSaved hook --- @@ -7603,10 +7602,7 @@ async fn hook_checkpoint_saved_fires() { assert_eq!(outcome.status, StageStatus::Success); // Checkpoint is saved after each node - assert!( - marker.exists(), - "CheckpointSaved hook should have fired" - ); + assert!(marker.exists(), "CheckpointSaved hook should have fired"); let content = std::fs::read_to_string(&marker).unwrap(); assert!( content.contains("work"), @@ -7735,15 +7731,23 @@ event = "run_complete" command = "echo done" "#; - let cfg: arc_workflows::cli::run_config::WorkflowRunConfig = - toml::from_str(toml).unwrap(); + let cfg: arc_workflows::cli::run_config::WorkflowRunConfig = toml::from_str(toml).unwrap(); assert_eq!(cfg.hooks.len(), 2); - assert_eq!(cfg.hooks[0].event, arc_workflows::hook::HookEvent::StageStart); + assert_eq!( + cfg.hooks[0].event, + arc_workflows::hook::HookEvent::StageStart + ); assert_eq!(cfg.hooks[0].matcher.as_deref(), Some("agent_loop")); assert!(cfg.hooks[0].is_blocking()); assert!(!cfg.hooks[0].runs_in_sandbox()); - assert_eq!(cfg.hooks[0].timeout(), std::time::Duration::from_millis(30000)); - assert_eq!(cfg.hooks[1].event, arc_workflows::hook::HookEvent::RunComplete); + assert_eq!( + cfg.hooks[0].timeout(), + std::time::Duration::from_millis(30000) + ); + assert_eq!( + cfg.hooks[1].event, + arc_workflows::hook::HookEvent::RunComplete + ); assert!(!cfg.hooks[1].is_blocking()); // RunComplete non-blocking by default } @@ -7752,10 +7756,7 @@ command = "echo done" #[tokio::test] async fn hook_blocking_override_makes_non_blocking_event_blocking() { // StageComplete is non-blocking by default, but force it to blocking - let mut hook = make_hook( - arc_workflows::hook::HookEvent::StageComplete, - "exit 1", - ); + let mut hook = make_hook(arc_workflows::hook::HookEvent::StageComplete, "exit 1"); hook.blocking = Some(true); let hooks = vec![hook]; @@ -7777,10 +7778,7 @@ async fn hook_blocking_override_makes_non_blocking_event_blocking() { #[tokio::test] async fn hook_non_blocking_override_on_blocking_event() { // RunStart is blocking by default, but force it to non-blocking - let mut hook = make_hook( - arc_workflows::hook::HookEvent::RunStart, - "exit 1", - ); + let mut hook = make_hook(arc_workflows::hook::HookEvent::RunStart, "exit 1"); hook.blocking = Some(false); let hooks = vec![hook]; @@ -7818,11 +7816,19 @@ async fn hook_matcher_regex_pattern() { // Both step1 and step2 should be skipped assert!( - !dir.path().join("nodes").join("step1").join("response.md").exists(), + !dir.path() + .join("nodes") + .join("step1") + .join("response.md") + .exists(), "step1 should be skipped by regex ^step" ); assert!( - !dir.path().join("nodes").join("step2").join("response.md").exists(), + !dir.path() + .join("nodes") + .join("step2") + .join("response.md") + .exists(), "step2 should be skipped by regex ^step" ); } @@ -7857,7 +7863,10 @@ async fn hook_json_block_with_reason() { let result = engine.run(&graph, &config).await; assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("forbidden by policy")); + assert!(result + .unwrap_err() + .to_string() + .contains("forbidden by policy")); } // --- Sandbox field tests --- @@ -7908,21 +7917,29 @@ max_tool_rounds = 10 timeout_ms = 120000 "#; - let cfg: arc_workflows::cli::run_config::WorkflowRunConfig = - toml::from_str(toml).unwrap(); + let cfg: arc_workflows::cli::run_config::WorkflowRunConfig = toml::from_str(toml).unwrap(); assert_eq!(cfg.hooks.len(), 2); // Prompt hook - assert_eq!(cfg.hooks[0].event, arc_workflows::hook::HookEvent::StageStart); + assert_eq!( + cfg.hooks[0].event, + arc_workflows::hook::HookEvent::StageStart + ); assert!(matches!( cfg.hooks[0].resolved_hook_type().as_deref(), Some(arc_workflows::hook::HookType::Prompt { prompt, model }) if prompt == "Should this stage proceed?" && *model == Some("haiku".into()) )); - assert_eq!(cfg.hooks[0].timeout(), std::time::Duration::from_millis(30000)); + assert_eq!( + cfg.hooks[0].timeout(), + std::time::Duration::from_millis(30000) + ); // Agent hook - assert_eq!(cfg.hooks[1].event, arc_workflows::hook::HookEvent::RunComplete); + assert_eq!( + cfg.hooks[1].event, + arc_workflows::hook::HookEvent::RunComplete + ); assert!(matches!( cfg.hooks[1].resolved_hook_type().as_deref(), Some(arc_workflows::hook::HookType::Agent { prompt, model, max_tool_rounds }) @@ -7930,7 +7947,10 @@ timeout_ms = 120000 && *model == Some("sonnet".into()) && *max_tool_rounds == Some(10) )); - assert_eq!(cfg.hooks[1].timeout(), std::time::Duration::from_millis(120000)); + assert_eq!( + cfg.hooks[1].timeout(), + std::time::Duration::from_millis(120000) + ); } // --- Prompt/Agent hook E2E with real LLM --- @@ -7987,7 +8007,10 @@ async fn hook_prompt_block_prevents_run() { let config = make_run_config(dir.path()); let result = engine.run(&graph, &config).await; - assert!(result.is_err(), "Prompt hook block should cause error, got: {result:?}"); + assert!( + result.is_err(), + "Prompt hook block should cause error, got: {result:?}" + ); } #[tokio::test] @@ -8083,7 +8106,10 @@ async fn hooks_do_not_duplicate_workflow_events() { .iter() .filter(|e| matches!(e, WorkflowRunEvent::WorkflowRunCompleted { .. })) .count(); - assert_eq!(run_completed, 1, "Should have exactly 1 WorkflowRunCompleted"); + assert_eq!( + run_completed, 1, + "Should have exactly 1 WorkflowRunCompleted" + ); // No WorkflowRunFailed let run_failed = captured