mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
## Summary Template failures from `fabro run` and structural warnings from `fabro validate` now preserve source provenance through rendering, workflow transforms, API serialization, and CLI display. Diagnostics can point at the actual workflow, import, or prompt file with node/attribute context instead of surfacing MiniJinja's generic `<string>` source. ## What Changed - Added named MiniJinja render APIs plus miette-aware `TemplateError` metadata for source names, source text, spans, and labels. - Reworked workflow template expansion so inline attributes, imported workflows, and `@prompt` files render with file and owner context. - Split strict run behavior from structural validate behavior: run-start still hard-fails on missing inputs, while validate emits source-aware warnings and continues linting. - Extended validation diagnostics through Rust structs, OpenAPI, server DTO mapping, and CLI rendering with optional source path, line, column, span, and related metadata. - Added regression coverage across template rendering, workflow transforms, CLI output, and the server validate endpoint. ## Verification - `cargo nextest run -p fabro-template` - `ulimit -n 4096 && cargo nextest run -p fabro-workflow --no-fail-fast` - `cargo nextest run -p fabro-cli bare_fabro_with_unbound_inputs_validates_structurally_with_warning run_rejects_unbound_template_inputs_before_creating_remote_run` - `cargo nextest run -p fabro-server validate_endpoint_returns_template_source_coordinates` - `cargo build -p fabro-api` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings` --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Aleksi Asikainen <1086393+salieri@users.noreply.github.com>
105 lines
3.9 KiB
Rust
105 lines
3.9 KiB
Rust
use fabro_test::{fabro_snapshot, test_context};
|
||
use serde_json::Value;
|
||
|
||
use super::support::fixture;
|
||
|
||
#[test]
|
||
fn help() {
|
||
let context = test_context!();
|
||
let mut cmd = context.command();
|
||
cmd.args(["preflight", "--help"]);
|
||
fabro_snapshot!(context.filters(), cmd, @"
|
||
success: true
|
||
exit_code: 0
|
||
----- stdout -----
|
||
Validate run configuration without executing
|
||
|
||
Usage: fabro preflight [OPTIONS] <WORKFLOW>
|
||
|
||
Arguments:
|
||
<WORKFLOW> Path to a .fabro workflow file or .toml task config
|
||
|
||
Options:
|
||
--json Output as JSON [env: FABRO_JSON=]
|
||
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
|
||
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
|
||
-I, --input <KEY=VALUE> Override a workflow input value (repeatable, format: KEY=VALUE)
|
||
--goal <GOAL> Override the workflow goal (available as {{ goal }} in prompts)
|
||
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||
--goal-file <GOAL_FILE> Read the workflow goal from a file
|
||
--quiet Suppress non-essential output [env: FABRO_QUIET=]
|
||
--model <MODEL> Override default LLM model
|
||
--provider <PROVIDER> Override default LLM provider
|
||
-v, --verbose Enable verbose output
|
||
--sandbox <SANDBOX> Sandbox for agent tools [possible values: local, docker, daytona]
|
||
-h, --help Print help
|
||
----- stderr -----
|
||
");
|
||
}
|
||
|
||
#[test]
|
||
fn preflight_invalid_workflow_fails_with_validation_output() {
|
||
let context = test_context!();
|
||
let workflow = fixture("invalid.fabro");
|
||
let mut cmd = context.command();
|
||
cmd.args(["preflight", workflow.to_str().unwrap()]);
|
||
|
||
fabro_snapshot!(context.filters(), cmd, @"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
----- stderr -----
|
||
Workflow: Invalid (2 nodes, 1 edges)
|
||
Graph: [FIXTURES]/invalid.fabro
|
||
error: Pipeline must have exactly one start node (shape=Mdiamond or id start/Start) (start_node)
|
||
error [node: exit]: Exit node 'exit' has 1 outgoing edge(s) but must have none (exit_no_outgoing)
|
||
× Validation failed
|
||
");
|
||
}
|
||
|
||
#[test]
|
||
fn preflight_rejects_unbound_template_inputs() {
|
||
let context = test_context!();
|
||
let workflow = fixture("templated_unbound.fabro");
|
||
let mut cmd = context.command();
|
||
cmd.args(["preflight", workflow.to_str().unwrap()]);
|
||
|
||
fabro_snapshot!(context.filters(), cmd, @"
|
||
success: false
|
||
exit_code: 1
|
||
----- stdout -----
|
||
----- stderr -----
|
||
Workflow: TemplatedUnbound (3 nodes, 2 edges)
|
||
Graph: [FIXTURES]/templated_unbound.fabro
|
||
Goal: Demo
|
||
|
||
error: [FIXTURES]/templated_unbound.fabro:2:26: undefined template variable `inputs.app_dir` in graph attribute `goal` (template_undefined_variable)
|
||
error: [FIXTURES]/templated_unbound.fabro:7:44: undefined template variable `inputs.app_dir` in node `work` attribute `prompt` [node: work] (template_undefined_variable)
|
||
× Validation failed
|
||
");
|
||
}
|
||
|
||
#[test]
|
||
fn preflight_invalid_workflow_json_emits_diagnostics() {
|
||
let context = test_context!();
|
||
let workflow = fixture("invalid.fabro");
|
||
let output = context
|
||
.command()
|
||
.args(["--json", "preflight", workflow.to_str().unwrap()])
|
||
.output()
|
||
.expect("command should run");
|
||
|
||
assert!(!output.status.success());
|
||
let value: Value =
|
||
serde_json::from_slice(&output.stdout).expect("preflight --json should parse");
|
||
assert_eq!(value["workflow"]["name"], "Invalid");
|
||
assert!(
|
||
value["workflow"]["diagnostics"]
|
||
.as_array()
|
||
.is_some_and(|diagnostics| !diagnostics.is_empty())
|
||
);
|
||
assert_eq!(value["checks"]["title"], "Run Preflight");
|
||
|
||
let stderr = String::from_utf8(output.stderr).unwrap();
|
||
assert!(stderr.contains("Validation failed"));
|
||
}
|