diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8f3423e7e..53d09d5c1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -169,6 +169,9 @@ jobs: # integration tests. - run: cargo nextest run --locked --profile ci --status-level slow -p fabro-sandbox --test plugin_provider - run: cargo nextest run --locked --profile ci --status-level slow --run-ignored only -p fabro-sandbox --test docker_streaming + # The Petri runs (not ignored: they skip without the host plugin, which + # the environment above forbids). + - run: cargo nextest run --locked --profile ci --status-level slow -p fabro-petri - run: cargo nextest run --locked --profile ci --status-level slow --run-ignored only -p fabro-workflow --test it -E 'test(asset_collection_docker_sandbox)' test-macos: diff --git a/AGENTS.md b/AGENTS.md index 05ca4f0ad..3e417f116 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,6 +125,7 @@ Fabro is an AI-powered workflow orchestration platform. Workflows are defined as - **fabro-cli** — CLI entry point. Commands: `run`, `exec`, `serve`, `validate`, `parse`, `cp`, `model`, `doctor`, `install`, `ps`, `system prune` - **fabro-workflow** — Core workflow engine. Parses Graphviz graphs, runs stages, manages checkpoints/resume, hooks, and human-in-the-loop interactions - **fabro-sandbox** — Local, Docker, and Daytona sandbox providers. `RunSandbox` is also the `Environment` pebble's coding agent runs its tools through; agent stages, Ask Fabro, hook evaluators, and `fabro exec` all run on the `pebble-coding-agent` crate (pinned by rev in the workspace `Cargo.toml`). `RunSandbox` is also the `Environment` pebble's coding agent runs its tools through; agent stages, Ask Fabro, hook evaluators, and `fabro exec` all run on the `pebble-coding-agent` crate (pinned by rev in the workspace `Cargo.toml`). Docker is the default runtime provider and creates clone-based `/workspace` containers through the operator's Docker daemon; Daytona uses the same GitHub-only clone-source contract. Docker daemon access is host-root-equivalent and assumes trusted callers/payloads. +- **fabro-petri** — Fabro's adapters over Petri, the workflow engine: the one crate that imports the Petri packages (pinned by rev in the workspace `Cargo.toml`), holding the run store over SQLite and the platform adapters - **fabro-server** — Axum HTTP server. Routes for runs, sessions, models, completions, usage. SSE event streaming. Demo mode via header - **fabro-llm** — Unified LLM client with providers: Anthropic, OpenAI, Gemini, OpenAI-compatible, plus retry/middleware/streaming - **fabro-api** — Auto-generated Rust types and reqwest HTTP client from OpenAPI spec (build.rs + progenitor) diff --git a/Cargo.lock b/Cargo.lock index f1ce43f02..cada5b097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,6 +248,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.6" @@ -1102,6 +1108,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "bytes", + "cfg_aliases", +] + [[package]] name = "brotli" version = "8.0.1" @@ -2087,6 +2103,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + [[package]] name = "document-features" version = "0.2.12" @@ -2860,6 +2882,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "fabro-petri" +version = "0.357.0-nightly.0" +dependencies = [ + "petri-attractor-steps", + "petri-execution", + "petri-frontend-attractor", + "petri-frontend-fabro", + "petri-runtime", + "petri-store", + "petri-testkit", + "tempfile", + "tokio", +] + [[package]] name = "fabro-proc" version = "0.357.0-nightly.0" @@ -4020,6 +4057,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "hashlink" version = "0.11.1" @@ -5022,6 +5068,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "marked-yaml" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a76cf4e66a8ffccfce983161b0faafe61a5ef03fe875ef2e3deb897e4e915fa" +dependencies = [ + "doc-comment", + "hashlink 0.10.0", + "yaml-rust2", +] + [[package]] name = "matchers" version = "0.2.0" @@ -5952,6 +6009,272 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petri-attractor-steps" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "globset", + "jsonschema", + "lithos-llm", + "pebble-agent", + "pebble-coding-agent", + "petri-execution", + "petri-executor", + "petri-frontend", + "petri-frontend-attractor", + "petri-ir", + "petri-runtime", + "petri-steps", + "regex", + "reqwest 0.13.4", + "serde", + "serde_json", + "shlex", + "smol_str", + "tar", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "petri-driver" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "getrandom 0.3.4", + "petri-engine", + "petri-executor", + "petri-ir", + "petri-steps", + "petri-store", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "petri-engine" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "petri-ir", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", +] + +[[package]] +name = "petri-execution" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "petri-driver", + "petri-engine", + "petri-executor", + "petri-executor-sandbox", + "petri-ir", + "petri-runtime", + "petri-steps", + "petri-store", + "regex", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "petri-executor" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "libc", + "petri-ir", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "petri-executor-sandbox" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "petri-executor", + "petri-ir", + "sandbox-driver", + "sandbox-driver-daytona-config", + "sandbox-driver-docker-config", + "sandbox-driver-protocol", + "serde", + "serde_json", + "sha2 0.10.9", + "smol_str", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "petri-frontend" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "marked-yaml", + "petri-ir", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "yaml-rust2", +] + +[[package]] +name = "petri-frontend-attractor" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "minijinja", + "petri-frontend", + "petri-ir", + "regex", + "serde", + "serde_json", + "shlex", + "smol_str", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "petri-frontend-fabro" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "petri-frontend", + "petri-frontend-attractor", + "petri-ir", + "regex", + "serde", + "serde_json", + "shlex", + "smol_str", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "petri-frontend-native" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "petri-frontend", + "petri-ir", + "serde_json", + "smol_str", +] + +[[package]] +name = "petri-ir" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "regex", + "serde", + "serde_json", + "sha2 0.10.9", + "smol_str", + "thiserror 2.0.18", +] + +[[package]] +name = "petri-runtime" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "petri-driver", + "petri-engine", + "petri-executor", + "petri-executor-sandbox", + "petri-frontend", + "petri-frontend-native", + "petri-ir", + "petri-steps", + "petri-store", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "petri-steps" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "petri-executor", + "petri-ir", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "petri-store" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "getrandom 0.3.4", + "petri-ir", + "serde", + "serde_json", + "smol_str", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "petri-testkit" +version = "0.1.0" +source = "git+https://github.com/lithoscomputer/petri.git?rev=a0d2ceb3887287a460e69ae101e3c718aac75c9e#a0d2ceb3887287a460e69ae101e3c718aac75c9e" +dependencies = [ + "async-trait", + "petri-driver", + "petri-engine", + "petri-executor", + "petri-executor-sandbox", + "petri-ir", + "petri-steps", + "petri-store", + "serde", + "serde_json", + "smol_str", + "tokio", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -7599,6 +7922,16 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + [[package]] name = "socket2" version = "0.6.2" @@ -7649,7 +7982,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink", + "hashlink 0.11.1", "indexmap 2.13.0", "log", "memchr", @@ -9729,6 +10062,17 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.10.0", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 2a57149a5..404450a67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,18 @@ sandbox-driver-testing = { git = "https://github.com/lithoscomputer/sandbox-driv pebble-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "a39f43e26effdf99635eaf343f095c17157c9c93" } pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble", rev = "a39f43e26effdf99635eaf343f095c17157c9c93", features = ["mcp", "search-providers"] } pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "a39f43e26effdf99635eaf343f095c17157c9c93" } +# petri: the workflow engine Fabro runs its workflows on. Pinned by rev, the +# same way pebble and sandbox-driver are. Petri pins the same pebble, +# lithos-llm and sandbox-driver revisions as this file, so the workspace links +# one copy of each. Only `fabro-petri` may depend on these packages; the keys +# carry the `petri_` prefix so the crate names say where they come from. +petri_runtime = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-runtime" } +petri_execution = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-execution" } +petri_store = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-store" } +petri_attractor_steps = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-attractor-steps" } +petri_frontend_attractor = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-frontend-attractor" } +petri_frontend_fabro = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-frontend-fabro" } +petri_testkit = { git = "https://github.com/lithoscomputer/petri.git", rev = "a0d2ceb3887287a460e69ae101e3c718aac75c9e", package = "petri-testkit" } sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] } fork = "0.2" exec = "0.3" diff --git a/lib/components/fabro-petri/Cargo.toml b/lib/components/fabro-petri/Cargo.toml new file mode 100644 index 000000000..5259e102e --- /dev/null +++ b/lib/components/fabro-petri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "fabro-petri" +edition.workspace = true +version.workspace = true +publish = false +license.workspace = true +description = "Fabro's adapters over Petri, the workflow engine: the one place Fabro touches it" + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +petri_runtime.workspace = true +petri_execution.workspace = true +petri_store.workspace = true +petri_attractor_steps.workspace = true +petri_frontend_attractor.workspace = true +petri_frontend_fabro.workspace = true + +[dev-dependencies] +petri_testkit.workspace = true +tempfile = "3" +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/lib/components/fabro-petri/README.md b/lib/components/fabro-petri/README.md new file mode 100644 index 000000000..8e2a0eb43 --- /dev/null +++ b/lib/components/fabro-petri/README.md @@ -0,0 +1,33 @@ +# fabro-petri + +Fabro's adapters over Petri, the workflow engine Fabro runs its workflows on. + +## Layering rule + +Only this crate imports Petri. The workspace `Cargo.toml` pins the Petri +packages by revision under `petri_*` keys, and `fabro-petri` is the only +member that lists them as dependencies. Every other Fabro crate reaches the +engine through what this crate exports. A Petri pin move is therefore a change +to this crate and the lockfile, nothing else. + +## What it holds + +Every adapter the integration plan describes lands here: the run store over +Fabro's SQLite database, then the platform adapters (hooks, interviews, +secrets, output storage, run tools, the event projection). + +## How it is tested + +Integration tests live under `tests/`: + +- `runs.rs` runs the `hello` bundle in memory through `Runtime::standard()` + with the Fabro frontend and the model-free stub registry, then a + command-only workflow on the host sandbox through the real step registry. + The sandbox test skips, and says why, when the `sandbox-driver-host` plugin + executable is not on `PATH`. + +Run them with: + +```sh +ulimit -n 4096 && cargo nextest run -p fabro-petri +``` diff --git a/lib/components/fabro-petri/src/lib.rs b/lib/components/fabro-petri/src/lib.rs new file mode 100644 index 000000000..f1c1546cd --- /dev/null +++ b/lib/components/fabro-petri/src/lib.rs @@ -0,0 +1,17 @@ +//! Fabro's adapters over Petri, the workflow engine Fabro runs its workflows +//! on. +//! +//! This crate is the one place Fabro touches Petri. Every other Fabro crate +//! reaches the engine through the types and functions exported here and never +//! depends on a Petri package itself. That keeps the engine's API surface in +//! one crate, so a Petri pin move is a change to this crate alone. +//! +//! What lives here, as the integration plan lands it: +//! +//! - the run store over Fabro's SQLite database, so Petri's records are the +//! run's source of truth in Fabro's tables; +//! - the platform adapters: hooks, interviews, secrets, output storage, the run +//! tools, the event projection. +//! +//! The Petri packages are pinned by revision in the workspace `Cargo.toml` +//! under `petri_*` keys. diff --git a/lib/components/fabro-petri/tests/runs.rs b/lib/components/fabro-petri/tests/runs.rs new file mode 100644 index 000000000..54a1a7e9e --- /dev/null +++ b/lib/components/fabro-petri/tests/runs.rs @@ -0,0 +1,190 @@ +//! A Fabro workflow runs through Petri from this crate: the `hello` bundle in +//! memory on the stub registry, and a command-only workflow on the host +//! sandbox through the real step registry. +//! +//! Every run, stubbed or real, acquires its scope's environment through the +//! sandbox-driver host plugin, so both tests skip when that executable is not +//! found, unless `FABRO_REQUIRE_SANDBOX_PLUGINS` is set. Fabro's CI installs +//! the plugin on `PATH` in the sandbox-plugins job and requires it there. + +#![expect( + clippy::disallowed_methods, + reason = "the tests locate the plugin executable through the process environment" +)] +#![expect(clippy::print_stderr, reason = "a skipped test says why on its stderr")] + +use std::env; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +use petri_execution::host::{self, HostRun}; +use petri_execution::inspect::{self, RunInspection}; +use petri_frontend_fabro::Fabro; +use petri_runtime::executor::Retention; +use petri_runtime::frontend::CompileInputs; +use petri_runtime::ir::RunStatus; +use petri_runtime::{RunOptions, Runtime}; +use petri_store::{Access, MemoryRunStore, RunKey, RunStore as _}; +use tokio::fs; + +const HOST_PLUGIN: &str = "sandbox-driver-host"; +const HOST_PLUGIN_OVERRIDE: &str = "PETRI_SANDBOX_HOST_PLUGIN"; +const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_PLUGINS"; + +/// A command-only workflow: one script stage between start and exit. +const COMMAND_WORKFLOW: &str = r#"digraph Command { + graph [goal="Run one command"] + start [shape=Mdiamond] + exit [shape=Msquare] + say [shape=parallelogram, script="echo hello from petri"] + start -> say -> exit +}"#; + +const COMMAND_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n"; + +/// The `.fabro/workflows/hello` bundle checked into this repository. +fn hello_bundle() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../.fabro/workflows/hello") +} + +/// The host plugin as Petri's lookup finds it: the override variable, else +/// the executable on `PATH`. `None`, after saying so, when the test should +/// skip; a panic when the environment forbids a skip. +fn host_plugin() -> Option { + let found = env::var_os(HOST_PLUGIN_OVERRIDE) + .map(PathBuf::from) + .or_else(|| { + env::split_paths(&env::var_os("PATH")?) + .map(|dir| dir.join(HOST_PLUGIN)) + .find(|candidate| candidate.is_file()) + }); + if found.is_none() { + assert!( + env::var_os(REQUIRE_ENV).is_none(), + "{REQUIRE_ENV} is set, but {HOST_PLUGIN} is not on PATH and {HOST_PLUGIN_OVERRIDE} is unset" + ); + eprintln!("skipping: {HOST_PLUGIN} is not on PATH and {HOST_PLUGIN_OVERRIDE} is unset"); + } + found +} + +/// Write a bundle's files into `/.fabro/workflows/` so the +/// frontend sees a bundle root of its own, with no project settings layer +/// above it. Returns the workflow file. +async fn install_bundle(root: &Path, name: &str, files: &[(&str, &str)]) -> PathBuf { + let bundle = root.join(".fabro").join("workflows").join(name); + fs::create_dir_all(&bundle) + .await + .expect("the bundle directory is creatable"); + for (file, text) in files { + fs::write(bundle.join(file), text) + .await + .expect("the bundle file is writable"); + } + bundle.join("workflow.fabro") +} + +fn run_options(run_dir: &Path, key: &str) -> RunOptions { + let mut options = RunOptions::new(run_dir); + options.grace = Duration::from_secs(2); + options.retention = Retention::Never; + options.echo = false; + options.run_key = Some(RunKey::new(key)); + options +} + +/// Lower `workflow`, run it to completion, and inspect the run through its +/// store. +async fn run_workflow( + rt: &Runtime, + store: &MemoryRunStore, + key: &str, + workflow: &Path, +) -> RunInspection { + let lowered = rt + .check(workflow, None, None, &CompileInputs::new()) + .expect("the workflow file loads"); + let graph = lowered + .graph + .unwrap_or_else(|| panic!("the workflow lowers: {:?}", lowered.diagnostics)); + let host_run = HostRun::new(graph).with_children(lowered.children); + let report = host::run_configured(rt, host_run, |_, _| {}) + .await + .expect("the run completes"); + assert_eq!( + report.status, + RunStatus::Success, + "errors: {:?}; history: {:#?}", + report.state.errors(), + report.state.history() + ); + let logs = store + .open(&RunKey::new(key), Access::Read) + .await + .expect("the run opens for reading"); + inspect::inspect_run(&*logs) + .await + .expect("the stored run inspects") +} + +/// The `hello` bundle, whose one stage is a prompt, completes on the stub +/// registry with no model, and its record in the memory store says so. The +/// stubbed stages never run a command, but the run still takes its host +/// scope through the plugin. +#[tokio::test] +async fn the_hello_bundle_runs_in_memory_on_the_stub_registry() { + if host_plugin().is_none() { + return; + } + let root = tempfile::tempdir().expect("a temp dir"); + let bundle = hello_bundle(); + let workflow_text = fs::read_to_string(bundle.join("workflow.fabro")) + .await + .expect("the hello workflow is checked in"); + let settings_text = fs::read_to_string(bundle.join("workflow.toml")) + .await + .expect("the hello settings are checked in"); + let workflow = install_bundle(root.path(), "hello", &[ + ("workflow.fabro", &workflow_text), + ("workflow.toml", &settings_text), + ]) + .await; + let store = Arc::new(MemoryRunStore::new()); + let rt = petri_attractor_steps::register_stubs(Runtime::standard().frontend(Fabro::new())) + .store(store.clone()) + .options(run_options(&root.path().join("run"), "hello")); + + let inspection = run_workflow(&rt, &store, "hello", &workflow).await; + + assert!(inspection.complete, "{:?}", inspection.incomplete); + assert_eq!(inspection.status.as_deref(), Some("success")); + assert_eq!(inspection.run_key, RunKey::new("hello")); + assert_eq!(inspection.executions.len(), 1); +} + +/// A command-only workflow runs its script on the host sandbox through the +/// real step registry, and its record in the memory store says so. +#[tokio::test] +async fn a_command_workflow_runs_on_the_host_sandbox() { + if host_plugin().is_none() { + return; + } + let root = tempfile::tempdir().expect("a temp dir"); + let workflow = install_bundle(root.path(), "command", &[ + ("workflow.fabro", COMMAND_WORKFLOW), + ("workflow.toml", COMMAND_SETTINGS), + ]) + .await; + let store = Arc::new(MemoryRunStore::new()); + let rt = petri_attractor_steps::register(Runtime::standard().frontend(Fabro::new())) + .store(store.clone()) + .options(run_options(&root.path().join("run"), "command")); + + let inspection = run_workflow(&rt, &store, "command", &workflow).await; + + assert!(inspection.complete, "{:?}", inspection.incomplete); + assert_eq!(inspection.status.as_deref(), Some("success")); + assert_eq!(inspection.run_key, RunKey::new("command")); + assert_eq!(inspection.executions.len(), 1); +}