From d611ef2bb1c7fec8687de03be3823b883709ee86 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Sat, 12 Sep 2026 10:23:34 -0600 Subject: [PATCH] Port workflow version creation to Pebble native tools --- Cargo.lock | 2 + lib/apps/fabro-cli/tests/it/cmd/mcp.rs | 5 +- lib/components/fabro-workflow/Cargo.toml | 2 + .../src/handler/llm/fabro_tools.rs | 97 ++++++++++++++++++- 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a84eec268..df85b11db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3313,6 +3313,7 @@ dependencies = [ "fabro-api", "fabro-auth", "fabro-checkpoint", + "fabro-client", "fabro-config", "fabro-core", "fabro-dump", @@ -3337,6 +3338,7 @@ dependencies = [ "fabro-validate", "fabro-vault", "fabro-workflow", + "fabro-workflow-version", "futures", "git2", "hex", diff --git a/lib/apps/fabro-cli/tests/it/cmd/mcp.rs b/lib/apps/fabro-cli/tests/it/cmd/mcp.rs index 64181a159..c07daff5a 100644 --- a/lib/apps/fabro-cli/tests/it/cmd/mcp.rs +++ b/lib/apps/fabro-cli/tests/it/cmd/mcp.rs @@ -1858,7 +1858,10 @@ async fn mcp_workflow_version_validation_happens_before_auth_or_network() { serde_json::json!({"entrypoint":"workflow","files":{}}), ) .await; - assert_eq!(error, "entrypoint `workflow` is not present in workflow files"); + assert_eq!( + error, + "entrypoint `workflow` is not present in workflow files" + ); assert_mcp_run_tool_count(&client).await; client .shutdown() diff --git a/lib/components/fabro-workflow/Cargo.toml b/lib/components/fabro-workflow/Cargo.toml index 314a53e42..8574796f1 100644 --- a/lib/components/fabro-workflow/Cargo.toml +++ b/lib/components/fabro-workflow/Cargo.toml @@ -77,6 +77,8 @@ tempfile = "3" toml.workspace = true fabro-vault = { path = "../../foundation/fabro-vault" } [dev-dependencies] +fabro-client = { path = "../../foundation/fabro-client" } +fabro-workflow-version = { path = "../fabro-workflow-version" } fabro-llm = { path = "../fabro-llm", features = ["test-support"] } fabro-store = { path = "../fabro-store", features = ["test-support"] } fabro-auth = { path = "../../foundation/fabro-auth", features = ["test-support"] } diff --git a/lib/components/fabro-workflow/src/handler/llm/fabro_tools.rs b/lib/components/fabro-workflow/src/handler/llm/fabro_tools.rs index 67d3df89f..c2903a64a 100644 --- a/lib/components/fabro-workflow/src/handler/llm/fabro_tools.rs +++ b/lib/components/fabro-workflow/src/handler/llm/fabro_tools.rs @@ -1,4 +1,4 @@ -//! The Fabro run tools (`fabro_run_*`) as application tools a pebble coding +//! The Fabro workflow and run tools as application tools a pebble coding //! agent can call. use std::sync::Arc; @@ -66,6 +66,15 @@ pub(crate) async fn execute_fabro_run_tool( services: &FabroRunToolServices, ) -> fabro_tool::ToolResult { match name { + fabro_tool::FABRO_WORKFLOW_VERSION_CREATE_TOOL_NAME => { + let params = + parse_fabro_tool_args::(name, args)?; + let source = fabro_tool::ValidatedWorkflowVersionCreate::try_from(params)?; + let result = + fabro_tool::create_workflow_version(Arc::clone(&services.backend), source).await?; + let summary = fabro_tool::workflow_version_create_text(&result); + render_fabro_tool_result(&summary, &result) + } fabro_tool::FABRO_RUN_CREATE_TOOL_NAME => { let params = parse_fabro_tool_args::(name, args)?; ensure_current_run_parent(¶ms, services.current_run_id)?; @@ -196,3 +205,89 @@ where })?; Ok(format!("{summary}\n{json}")) } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use async_trait::async_trait; + use fabro_tool::fabro_client::ClientBackend; + use fabro_tool::{ValidatedWorkflowVersionCreate, WorkflowVersionPackager}; + use fabro_types::WorkflowVersion; + use fabro_workflow_version::{CollectedWorkflowClosure, ValidatedWorkflowVersion}; + use serde_json::json; + + use super::*; + + struct SingleGraphPackager; + + #[async_trait] + impl WorkflowVersionPackager for SingleGraphPackager { + async fn package( + &self, + source: ValidatedWorkflowVersionCreate, + ) -> anyhow::Result { + let version = WorkflowVersion::new(source.entrypoint, source.files, BTreeMap::new())?; + let id = version.id()?; + Ok(CollectedWorkflowClosure::from_dependency_order(id, vec![( + id, + ValidatedWorkflowVersion::new(version)?, + )])) + } + } + + #[tokio::test] + async fn workflow_version_native_dispatch_registers_and_returns_version() { + let server = httpmock::MockServer::start_async().await; + let version = WorkflowVersion::new( + "workflow".parse().unwrap(), + BTreeMap::from([("workflow".parse().unwrap(), "digraph W {}".into())]), + BTreeMap::new(), + ) + .unwrap(); + let id = version.id().unwrap(); + let upload = server + .mock_async(|when, then| { + when.method(httpmock::Method::POST) + .path("/api/v1/workflow-versions") + .json_body_obj(&version); + then.status(201) + .json_body(json!({"workflow_version_id": id})); + }) + .await; + let client = fabro_client::Client::new_no_proxy(&server.url("")).unwrap(); + let services = FabroRunToolServices { + backend: Arc::new( + ClientBackend::new(Arc::new(client)) + .with_workflow_version_packager(Arc::new(SingleGraphPackager)), + ), + current_run_id: "01KRBZW4DW0000000000000002".parse().unwrap(), + base_cwd: "unused".into(), + user_settings_path: "unused".into(), + }; + let name = fabro_tool::FABRO_WORKFLOW_VERSION_CREATE_TOOL_NAME; + assert_eq!(register_named_fabro_run_tools(&services, &[name]).len(), 1); + let output = execute_fabro_run_tool( + name, + json!({"entrypoint":"workflow", "files":{"workflow":"digraph W {}"}}), + &services, + ) + .await + .unwrap(); + let (summary, body) = output.split_once('\n').unwrap(); + assert_eq!(summary, format!("Registered workflow version {id}")); + assert_eq!( + serde_json::from_str::(body).unwrap(), + json!({"workflow_version_id": id}) + ); + let error = execute_fabro_run_tool( + name, + json!({"entrypoint":"missing", "files":{"workflow":"digraph W {}"}}), + &services, + ) + .await + .unwrap_err(); + assert!(error.to_string().contains("not present")); + upload.assert_calls_async(1).await; + } +}