use jsonschema::JSONSchema;
use klieo_workflow::{AgentConfig, Condition, EdgeDef, NodeDef, NodeKind, Op, WorkflowDef};
use serde_json::{json, Value};
use std::path::PathBuf;
const SCHEMA_PATH: &str = "docs/schemas/workflow/workflow-def.schema.json";
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
}
fn load_schema() -> Value {
let path = workspace_root().join(SCHEMA_PATH);
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("read schema {}: {err}", path.display()));
serde_json::from_str(&text)
.unwrap_or_else(|err| panic!("parse schema {}: {err}", path.display()))
}
fn compile_schema() -> JSONSchema {
JSONSchema::compile(&load_schema()).expect("workflow schema compiles as valid JSON Schema")
}
fn violations(schema: &JSONSchema, instance: &Value) -> Vec<String> {
match schema.validate(instance) {
Ok(()) => Vec::new(),
Err(errors) => errors.map(|error| error.to_string()).collect(),
}
}
fn agent_node(id: &str) -> NodeDef {
NodeDef {
id: id.into(),
kind: NodeKind::Agent {
agent: AgentConfig {
model: "default".into(),
system_prompt: "be brief".into(),
tools: vec!["lookup".into()],
},
},
input_from: Some("q".into()),
output_to: Some("a".into()),
}
}
fn tool_node(id: &str) -> NodeDef {
NodeDef {
id: id.into(),
kind: NodeKind::Tool {
tool: "lookup".into(),
},
input_from: Some("x".into()),
output_to: Some("y".into()),
}
}
fn subflow_node(id: &str, target: &str) -> NodeDef {
NodeDef {
id: id.into(),
kind: NodeKind::Subflow {
target: target.into(),
},
input_from: None,
output_to: None,
}
}
fn unconditional_edge(from: &str, to: &str) -> EdgeDef {
EdgeDef {
from: from.into(),
to: Some(to.into()),
when: None,
then: None,
otherwise: None,
}
}
fn conditional_edge(from: &str, op: Op) -> EdgeDef {
let value = if matches!(op, Op::Exists) {
Value::Null
} else {
json!(0.8)
};
EdgeDef {
from: from.into(),
to: None,
when: Some(Condition {
field: "risk".into(),
op,
value,
}),
then: Some("hi".into()),
otherwise: Some("lo".into()),
}
}
fn node_kind_fixtures() -> Vec<(String, WorkflowDef)> {
vec![
(
"agent_unconditional".to_string(),
WorkflowDef {
id: "w-agent".into(),
version: 2,
entry: "a".into(),
nodes: vec![agent_node("a"), agent_node("b")],
edges: vec![unconditional_edge("a", "b")],
},
),
(
"tool_node".to_string(),
WorkflowDef {
id: "w-tool".into(),
version: 0,
entry: "t".into(),
nodes: vec![tool_node("t")],
edges: vec![],
},
),
(
"subflow_node".to_string(),
WorkflowDef {
id: "w-sub".into(),
version: 0,
entry: "s".into(),
nodes: vec![subflow_node("s", "fraud")],
edges: vec![],
},
),
]
}
fn conditional_fixture(op: Op) -> (String, WorkflowDef) {
(
format!("conditional_{op:?}"),
WorkflowDef {
id: "w-cond".into(),
version: 1,
entry: "s".into(),
nodes: vec![
subflow_node("s", "seed"),
subflow_node("hi", "high"),
subflow_node("lo", "low"),
],
edges: vec![conditional_edge("s", op)],
},
)
}
fn fixtures() -> Vec<(String, WorkflowDef)> {
let mut out = node_kind_fixtures();
out.extend(Op::ALL.map(conditional_fixture));
out
}
#[test]
fn schema_compiles_as_valid_json_schema() {
compile_schema();
}
#[test]
fn every_fixture_validates_against_published_schema() {
let schema = compile_schema();
for (name, def) in fixtures() {
let instance = serde_json::to_value(&def).expect("WorkflowDef serialises to json");
let violations = violations(&schema, &instance);
assert!(
violations.is_empty(),
"fixture `{name}` violates the published schema: {violations:?}\ninstance = {instance}",
);
}
}
#[test]
fn schema_rejects_a_workflow_missing_required_fields() {
let schema = compile_schema();
let bad = json!({ "id": "x", "nodes": [], "bogus": true });
assert!(!violations(&schema, &bad).is_empty());
}
#[test]
fn schema_rejects_an_unknown_node_kind() {
let schema = compile_schema();
let bad = json!({
"id": "x", "entry": "n",
"nodes": [{ "id": "n", "kind": "loop", "body": "x" }]
});
assert!(!violations(&schema, &bad).is_empty());
}